From 29168df7cc30731d9dc2045a623c658f7fa260b4 Mon Sep 17 00:00:00 2001 From: Thomas Binsfeld Date: Tue, 18 Dec 2018 17:27:41 +0100 Subject: [PATCH] [REF] Contract: invoice creation [FIX] - Fix typo [IMP] - date start required for contract line [REF] Gitignore: .eggs [REF] Contract Unit Tests: base the cron check on invoice lines instead of invoices --- contract/models/contract.py | 165 ++++++++++++++++++++++++++----- contract/models/contract_line.py | 83 +++------------- contract/tests/test_contract.py | 9 +- 3 files changed, 162 insertions(+), 95 deletions(-) diff --git a/contract/models/contract.py b/contract/models/contract.py index 8a7dfd001..87156c85b 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -173,25 +173,20 @@ class AccountAnalyticAccount(models.Model): invoice_type = 'out_invoice' if self.contract_type == 'purchase': invoice_type = 'in_invoice' - invoice = self.env['account.invoice'].new( - { - 'reference': self.code, - 'type': invoice_type, - 'partner_id': self.partner_id.address_get(['invoice'])[ - 'invoice' - ], - 'currency_id': currency.id, - 'date_invoice': date_invoice, - 'journal_id': journal.id, - 'origin': self.name, - 'company_id': self.company_id.id, - 'contract_id': self.id, - 'user_id': self.partner_id.user_id.id, - } - ) - # Get other invoice values from partner onchange - invoice._onchange_partner_id() - return invoice._convert_to_write(invoice._cache) + return { + 'reference': self.code, + 'type': invoice_type, + 'partner_id': self.partner_id.address_get(['invoice'])[ + 'invoice' + ], + 'currency_id': currency.id, + 'date_invoice': date_invoice, + 'journal_id': journal.id, + 'origin': self.name, + 'company_id': self.company_id.id, + 'contract_id': self.id, + 'user_id': self.partner_id.user_id.id, + } @api.multi def action_contract_send(self): @@ -217,12 +212,136 @@ class AccountAnalyticAccount(models.Model): 'context': ctx, } + @api.model + def _finalize_invoice_values(self, invoice_values): + """ + This method adds the missing values in the invoice lines dictionaries. + + If no account on the product, the invoice lines account is + taken from the invoice's journal in _onchange_product_id + This code is not in finalize_creation_from_contract because it's + not possible to create an invoice line with no account + + :param invoice_values: dictionary (invoice values) + :return: updated dictionary (invoice values) + """ + # If no account on the product, the invoice lines account is + # taken from the invoice's journal in _onchange_product_id + # This code is not in finalize_creation_from_contract because it's + # not possible to create an invoice line with no account + new_invoice = self.env['account.invoice'].new(invoice_values) + for invoice_line in new_invoice.invoice_line_ids: + name = invoice_line.name + account_analytic_id = invoice_line.account_analytic_id + price_unit = invoice_line.price_unit + invoice_line.invoice_id = new_invoice + invoice_line._onchange_product_id() + invoice_line.update({ + 'name': name, + 'account_analytic_id': account_analytic_id, + 'price_unit': price_unit, + }) + return new_invoice._convert_to_write(new_invoice._cache) + + @api.model + def _finalize_invoice_creation(self, invoices): + for invoice in invoices: + invoice._onchange_partner_id() + invoices.compute_taxes() + + @api.model + def _finalize_and_create_invoices(self, invoices_values): + """ + This method: + - finalizes the invoices values (onchange's...) + - creates the invoices + - finalizes the created invoices (onchange's, tax computation...) + :param invoices_values: list of dictionaries (invoices values) + :return: created invoices (account.invoice) + """ + if isinstance(invoices_values, dict): + invoices_values = [invoices_values] + final_invoices_values = [] + for invoice_values in invoices_values: + final_invoices_values.append( + self._finalize_invoice_values(invoice_values)) + invoices = self.env['account.invoice'].create(final_invoices_values) + self._finalize_invoice_creation(invoices) + return invoices + + @api.model + def _get_contracts_to_invoice_domain(self, date_ref=None): + """ + This method builds the domain to use to find all + contracts (account.analytic.account) to invoice. + :param date_ref: optional reference date to use instead of today + :return: list (domain) usable on account.analytic.account + """ + domain = [] + if not date_ref: + date_ref = fields.Date.context_today(self) + domain.extend( + [ + ('recurring_invoices', '=', True), + ('recurring_next_date', '<=', date_ref), + ] + ) + return domain + + @api.multi + def _get_lines_to_invoice(self, date_ref): + """ + This method fetches and returns the lines to invoice on the contract + (self), based on the given date. + :param date_ref: date used as reference date to find lines to invoice + :return: contract lines (account.analytic.invoice.line recordset) + """ + self.ensure_one() + return self.recurring_invoice_line_ids.filtered( + lambda l: not l.is_canceled and l.recurring_next_date + and l.recurring_next_date <= date_ref) + + @api.multi + def _prepare_recurring_invoices_values(self, date_ref=False): + """ + This method builds the list of invoices values to create, based on + the lines to invoice of the contracts in self. + !!! The date of next invoice (recurring_next_date) is updated here !!! + :return: list of dictionaries (invoices values) + """ + invoices_values = [] + for contract in self: + if not date_ref: + date_ref = contract.recurring_next_date + contract_lines = contract._get_lines_to_invoice(date_ref) + if not contract_lines: + continue + invoice_values = contract._prepare_invoice(date_ref) + for line in contract_lines: + invoice_values.setdefault('invoice_line_ids', []) + invoice_values['invoice_line_ids'].append( + (0, 0, line._prepare_invoice_line(invoice_id=False)) + ) + invoices_values.append(invoice_values) + contract_lines._update_recurring_next_date() + return invoices_values + @api.multi def recurring_create_invoice(self): - return self.env[ - 'account.analytic.invoice.line' - ].recurring_create_invoice(self) + """ + This method triggers the creation of the next invoices of the contracts + even if their next invoicing date is in the future. + """ + return self._recurring_create_invoice() + + @api.multi + def _recurring_create_invoice(self, date_ref=False): + invoices_values = self._prepare_recurring_invoices_values(date_ref) + return self._finalize_and_create_invoices(invoices_values) @api.model def cron_recurring_create_invoice(self): - self.env['account.analytic.invoice.line'].recurring_create_invoice() + domain = self._get_contracts_to_invoice_domain() + contracts_to_invoice = self.search(domain) + date_ref = fields.Date.context_today(contracts_to_invoice) + contracts_to_invoice._recurring_create_invoice(date_ref) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index f0fa15c85..c87abf911 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -24,6 +24,7 @@ class AccountAnalyticInvoiceLine(models.Model): ) date_start = fields.Date( string='Date Start', + required=True, default=lambda self: fields.Date.context_today(self), ) date_end = fields.Date(string='Date End', index=True) @@ -78,7 +79,10 @@ class AccountAnalyticInvoiceLine(models.Model): compute="_compute_state", ) active = fields.Boolean( - string="Active", related="contract_id.active", strore=True + string="Active", + related="contract_id.active", + store=True, + readonly=True, ) @api.multi @@ -312,75 +316,18 @@ class AccountAnalyticInvoiceLine(models.Model): rec.recurring_next_date ) - @api.model - def _get_recurring_create_invoice_domain(self, contract=False): - domain = [] - date_ref = fields.Date.context_today(self) - if contract: - contract.ensure_one() - date_ref = contract.recurring_next_date - domain.append(('contract_id', '=', contract.id)) - - domain.extend( - [ - ('contract_id.recurring_invoices', '=', True), - ('recurring_next_date', '<=', date_ref), - ('is_canceled', '=', False), - ] - ) - return domain - - @api.model - def recurring_create_invoice(self, contract=False): - domain = self._get_recurring_create_invoice_domain(contract) - contract_to_invoice = self.read_group( - domain, ['id', 'contract_id'], ['contract_id'] - ) - return self._recurring_create_invoice(contract_to_invoice) - - @api.model - def _recurring_create_invoice(self, contract_to_invoice): - """Create invoices from contracts - - :return: invoices created - """ - invoices = self.env['account.invoice'] - for contract in contract_to_invoice: - lines = self.search(contract['__domain']) - if lines: - invoices |= lines._create_invoice() - lines._update_recurring_next_date() - return invoices - @api.multi - def _create_invoice(self): - """ - :return: invoice created - """ - contract = self.mapped('contract_id') - date_invoice = min(self.mapped('recurring_next_date')) - invoice = self.env['account.invoice'].create( - contract._prepare_invoice(date_invoice) - ) - for line in self: - invoice_line_vals = line._prepare_invoice_line(invoice.id) - if invoice_line_vals: - self.env['account.invoice.line'].create(invoice_line_vals) - invoice.compute_taxes() - return invoice - - @api.multi - def _prepare_invoice_line(self, invoice_id): + def _prepare_invoice_line(self, invoice_id=False): self.ensure_one() - invoice_line = self.env['account.invoice.line'].new( - { - 'invoice_id': invoice_id, - 'product_id': self.product_id.id, - 'quantity': self.quantity, - 'uom_id': self.uom_id.id, - 'discount': self.discount, - } - ) + invoice_line_vals = { + 'product_id': self.product_id.id, + 'quantity': self.quantity, + 'uom_id': self.uom_id.id, + 'discount': self.discount, + } + if invoice_id: + invoice_line_vals['invoice_id'] = invoice_id.id + invoice_line = self.env['account.invoice.line'].new(invoice_line_vals) # Get other invoice line values from product onchange invoice_line._onchange_product_id() invoice_line_vals = invoice_line._convert_to_write(invoice_line._cache) diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index d427692fd..b1482d032 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -1290,14 +1290,15 @@ class TestContract(TestContractBase): self.acct_line.recurring_invoicing_type = 'post-paid' self.acct_line.date_end = '2018-03-15' self.acct_line._onchange_date_start() - contracts = self.contract + contracts = self.contract2 for i in range(10): contracts |= self.contract.copy() self.env['account.analytic.account'].cron_recurring_create_invoice() - invoices = self.env['account.invoice'].search( - [('contract_id', 'in', contracts.ids)] + invoice_lines = self.env['account.invoice.line'].search( + [('account_analytic_id', 'in', contracts.ids)] ) - self.assertEqual(len(contracts), len(invoices)) + self.assertEqual(len(contracts.mapped('recurring_invoice_line_ids')), + len(invoice_lines)) def test_get_invoiced_period_monthlylastday(self): self.acct_line.date_start = '2018-01-05'