Skip to content

Commit

Permalink
Merge pull request #129 from CompassionCH/devel
Browse files Browse the repository at this point in the history
2021-06-10 Release
  • Loading branch information
ecino authored Jun 10, 2021
2 parents f0e7291 + e603f01 commit d520f2f
Show file tree
Hide file tree
Showing 7 changed files with 383 additions and 111 deletions.
4 changes: 2 additions & 2 deletions recurring_contract/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
{
'name': 'Recurring contract',
'summary': 'Contract for recurring invoicing',
'version': '12.0.1.0.0',
'version': '12.0.1.1.0',
'license': 'AGPL-3',
'author': 'Compassion CH',
'development_status': 'Stable',
'development_status': 'Production/Stable',
'website': 'http://www.compassion.ch',
'category': 'Accounting',
'depends': [
Expand Down
1 change: 1 addition & 0 deletions recurring_contract/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from . import queue_job
from . import utm
from . import end_reason
from . import move_line
45 changes: 33 additions & 12 deletions recurring_contract/models/contract_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def do_nothing(self):
pass

@api.multi
def generate_invoices(self, invoicer=None):
def generate_invoices(self, invoicer=None, cancelled_invoices=None):
""" By default, launch asynchronous job to perform the task.
Context value async_mode set to False can force to perform
the task immediately.
Expand All @@ -149,9 +149,10 @@ def generate_invoices(self, invoicer=None):
delay = datetime.today()
if jobs:
delay += relativedelta(minutes=1)
self.with_delay(eta=delay)._generate_invoices(invoicer)
self.with_delay(eta=delay)._generate_invoices(
invoicer, cancelled_invoices=cancelled_invoices)
else:
self._generate_invoices(invoicer)
self._generate_invoices(invoicer, cancelled_invoices=cancelled_invoices)
return invoicer

@api.multi
Expand Down Expand Up @@ -179,13 +180,15 @@ def get_relative_delta(self):
@api.multi
@job(default_channel='root.recurring_invoicer')
@related_action(action='related_action_invoicer')
def _generate_invoices(self, invoicer=None):
def _generate_invoices(self, invoicer=None, cancelled_invoices=None):
""" Checks all contracts and generate invoices if needed.
Create an invoice per contract group per date.
"""
logger.info("Invoice generation started.")
if invoicer is None:
invoicer = self.env['recurring.invoicer'].create({})
if cancelled_invoices is None:
cancelled_invoices = self.env["account.invoice"]
inv_obj = self.env['account.invoice']
gen_states = self._get_gen_states()
journal = self.env['account.journal'].search([
Expand Down Expand Up @@ -216,9 +219,21 @@ def _generate_invoices(self, invoicer=None):
if not contracts:
break
try:
inv_to_reopen = cancelled_invoices.filtered(
lambda inv: inv.date_invoice == current_date)

inv_data = contract_group._setup_inv_data(
journal, invoicer, contracts)
invoice = inv_obj.create(inv_data)
if not inv_to_reopen:
invoice = inv_obj.create(inv_data)
else:
inv_to_reopen.action_invoice_draft()
inv_to_reopen.env.clear()
old_lines = inv_to_reopen.invoice_line_ids.filtered(
lambda line: line.contract_id.id in contracts.ids)
old_lines.unlink()
inv_to_reopen.write(inv_data)
invoice = inv_to_reopen
if invoice.invoice_line_ids:
invoice.action_invoice_open()
else:
Expand Down Expand Up @@ -248,13 +263,19 @@ def _clean_generate_invoices(self):
"""
res = self.env['account.invoice']
for group in self:
since_date = date.today()
if group.last_paid_invoice_date:
last_paid_invoice_date = group.last_paid_invoice_date
since_date = max(since_date, last_paid_invoice_date)
group.contract_ids.with_context(async_mode=False).rewind_next_invoice_date()

# invoice for current contract might not be up to date.
# since we are changing the value of next_invoice_date
# this might cause some issue if we don't first generate the missing one.
if group.next_invoice_date and group.next_invoice_date <= date.today():
group._generate_invoices()

res |= group.contract_ids.with_context(
async_mode=False).rewind_next_invoice_date()
# Generate again invoices
self._generate_invoices()
self._generate_invoices(invoicer=None, cancelled_invoices=res.filtered(
lambda inv: inv.state == "cancel"
))
return res

@api.multi
Expand Down Expand Up @@ -288,7 +309,7 @@ def _setup_inv_data(self, journal, invoicer, contracts):
'payment_term_id': self.env.ref(
'account.account_payment_term_immediate').id,
'currency_id':
partner.property_product_pricelist.currency_id.id,
partner.property_product_pricelist.currency_id.id,
'date_invoice': self.next_invoice_date,
'recurring_invoicer_id': invoicer.id,
'payment_mode_id': self.payment_mode_id.id,
Expand Down
35 changes: 29 additions & 6 deletions recurring_contract/models/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
#
##############################################################################

from odoo import fields, models, api
from odoo import fields, models, api, _
from odoo.exceptions import UserError
from datetime import date


Expand Down Expand Up @@ -46,6 +47,9 @@ def reconcile_after_clean(self):
- amount for reconciling the future invoices
- leftover amount that will stay in the client balance
Then the invoices will be reconciled again
Invoices should be opened or canceled. if they are canceled they will
first be reopened
:return: True
"""
# At first we open again the cancelled invoices
Expand All @@ -72,9 +76,12 @@ def reconcile_after_clean(self):
])
for payment in open_payments:
if not is_past_reconciled and payment.credit == past_amount:
past_invoices.mapped('')
lines = past_invoices.mapped('move_id.line_ids').filtered("debit")
(lines + payment).reconcile()
is_past_reconciled = True
elif not is_future_reconciled:
elif not is_future_reconciled and payment.credit == future_amount:
lines = future_invoices.mapped('move_id.line_ids').filtered("debit")
(lines + payment).reconcile()
is_future_reconciled = True

# If no matching payment found, we will group or split.
Expand Down Expand Up @@ -108,13 +115,14 @@ def _group_or_split_reconcile(self):
order='date asc', limit=1)
if payment_greater_than_reconcile:
# Split the payment move line to isolate reconcile amount
(payment_greater_than_reconcile | move_lines) \
return (payment_greater_than_reconcile | move_lines)\
.split_payment_and_reconcile()
return True
else:
# Group several payments to match the invoiced amount
# Limit to 12 move_lines to avoid too many computations
open_payments = line_obj.search(payment_search, limit=12)
if sum(open_payments.mapped("credit")) < reconcile_amount:
raise UserError(_("Cannot reconcile invoices, not enough credit."))

# Search for a combination giving the invoiced amount recursively
# https://stackoverflow.com/questions/4632322/finding-all-possible-
Expand Down Expand Up @@ -149,7 +157,7 @@ def find_sum(numbers, target, partial=None):
if payment_line.credit > missing_amount:
# Split last added line amount to perfectly match
# the total amount we are looking for
return (open_payments[:index + 1] | move_lines) \
return (open_payments[:index + 1] | move_lines)\
.split_payment_and_reconcile()
payment_amount += payment_line.credit
return (open_payments | move_lines).reconcile()
Expand All @@ -169,3 +177,18 @@ class AccountInvoiceLine(models.Model):
state = fields.Selection(
related='invoice_id.state',
readonly=True, store=True)

@api.multi
def filter_for_contract_rewind(self, filter_state):
"""
Returns a subset of invoice lines that should be used to find after which one
we will set the next_invoice_date of a contract.
:param filter_state: filter invoice lines that have the desired state
:return: account.invoice.line recordset
"""
company = self.mapped("contract_id.company_id")
lock_date = company.period_lock_date
return self.filtered(
lambda l: l.state == filter_state and
(not lock_date or l.due_date > lock_date)
)
79 changes: 79 additions & 0 deletions recurring_contract/models/move_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
##############################################################################
#
# Copyright (C) 2014-2018 Compassion CH (http://www.compassion.ch)
# Releasing children from poverty in Jesus' name
# @author: Emanuel Cino <[email protected]>
#
# The licence is in the file __manifest__.py
#
##############################################################################

from odoo import models, exceptions, _


class MoveLine(models.Model):
""" Adds a method to split a payment into several move_lines
in order to reconcile only a partial amount, avoiding doing
partial reconciliation. """

_inherit = "account.move.line"

def split_payment_and_reconcile(self):
sum_credit = sum(self.mapped("credit"))
sum_debit = sum(self.mapped("debit"))
if sum_credit == sum_debit:
# Nothing to do here
return self.reconcile()

# Check in which direction we are reconciling
split_column = "credit" if sum_credit > sum_debit else "debit"
difference = abs(sum_credit - sum_debit)

for line in self:
if getattr(line, split_column) > difference:
# We will split this line
move = line.move_id
move_line = line
break
else:
raise exceptions.UserError(
_(
"This can only be done if one move line can be split "
"to cover the reconcile difference"
)
)

# Edit move in order to split payment into two move lines
payment = move_line.payment_id
if payment:
payment_lines = payment.move_line_ids
payment.move_line_ids = False
move.button_cancel()
move.write(
{
"line_ids": [
(1, move_line.id, {split_column: move_line.credit - difference}),
(
0,
0,
{
split_column: difference,
"name": self.env.context.get(
"residual_comment", move_line.name
),
"account_id": move_line.account_id.id,
"date": move_line.date,
"date_maturity": move_line.date_maturity,
"journal_id": move_line.journal_id.id,
"partner_id": move_line.partner_id.id,
},
),
]
}
)
move.post()
if payment:
payment.move_line_ids = payment_lines

# Perform the reconciliation
return self.reconcile()
Loading

0 comments on commit d520f2f

Please sign in to comment.