From d5b4534bf930481d6f6dd34735050842ff084039 Mon Sep 17 00:00:00 2001 From: Horacio Tellez Date: Mon, 11 Apr 2022 08:45:23 +0000 Subject: [PATCH] [IMP] sale: Only record down payments when confirmed Until now, there was no way on a given sales order to distinguish draft, cancelled and posted down payments. This commit makes sure the down payments state is clearly shown as draft or cancelled when the linked invoice is in the corresponding states. Also remove those draft and cancelled down payments from the sale portal & report as it can be confusing for customers to see line(s) of down payments they never heard of. Lastly, follow the existing behavior of invoices, where the down payments were shown in a separate section. Now a dedicated section will be added to the sales order and propagated to the invoice, if there are down payments to deduct. Task - 2702407 closes odoo/odoo#82376 Signed-off-by: Victor Feyens (vfe) --- addons/sale/models/account_move.py | 26 +++++++++++++++++-- addons/sale/models/sale_order.py | 19 ++++++++++++++ addons/sale/models/sale_order_line.py | 26 ++++++++++++++++++- .../report/ir_actions_report_templates.xml | 9 ++++--- addons/sale/tests/test_sale_refund.py | 6 +++-- addons/sale/tests/test_sale_to_invoice.py | 7 ++--- addons/sale/views/sale_order_views.xml | 5 ++-- addons/sale/views/sale_portal_templates.xml | 8 +++--- .../sale/wizard/sale_make_invoice_advance.py | 22 +++++++++++++++- 9 files changed, 109 insertions(+), 19 deletions(-) diff --git a/addons/sale/models/account_move.py b/addons/sale/models/account_move.py index ad38e69cd33..8c0bcb70fd7 100644 --- a/addons/sale/models/account_move.py +++ b/addons/sale/models/account_move.py @@ -61,8 +61,12 @@ class AccountMove(models.Model): def action_post(self): #inherit of the function from account.move to validate a new tax and the priceunit of a downpayment res = super(AccountMove, self).action_post() - line_ids = self.mapped('line_ids').filtered(lambda line: line.sale_line_ids.is_downpayment) - for line in line_ids: + down_payment_lines = self.line_ids.filtered(lambda line: line.sale_line_ids.is_downpayment) + for line in down_payment_lines: + + if not line.sale_line_ids.display_type: + line.sale_line_ids._compute_name() + try: line.sale_line_ids.tax_id = line.tax_ids if all(line.tax_ids.mapped('price_include')): @@ -77,6 +81,24 @@ class AccountMove(models.Model): pass return res + def button_draft(self): + res = super().button_draft() + + self.line_ids.filtered( + lambda line: line.sale_line_ids.is_downpayment and not line.sale_line_ids.display_type + ).sale_line_ids._compute_name() + + return res + + def button_cancel(self): + res = super().button_cancel() + + self.line_ids.filtered( + lambda line: line.sale_line_ids.is_downpayment and not line.sale_line_ids.display_type + ).sale_line_ids._compute_name() + + return res + def _post(self, soft=True): # OVERRIDE # Auto-reconcile the invoice with payments coming from transactions. diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index d487bf521c2..3c510182b9b 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -1245,6 +1245,25 @@ class SaleOrder(models.Model): self.ensure_one() return self.transaction_ids._get_last() + def _get_order_lines_to_report(self): + down_payment_lines = self.order_line.filtered(lambda line: + line.is_downpayment + and not line.display_type + and not line._get_downpayment_state() + ) + + def show_line(line): + if not line.is_downpayment: + return True + elif line.display_type and down_payment_lines: + return True # Only show the down payment section if down payments were posted + elif line in down_payment_lines: + return True # Only show posted down payments + else: + return False + + return self.order_line.filtered(show_line) + # PORTAL # def has_to_be_signed(self, include_draft=False): diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 48f7c46640e..4195cec493d 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -291,7 +291,17 @@ class SaleOrderLine(models.Model): for line in self: if not line.product_id: continue - line.name = line.with_context(lang=line.order_partner_id.lang)._get_sale_order_line_multiline_description_sale() + + name = line.with_context(lang=line.order_partner_id.lang)._get_sale_order_line_multiline_description_sale() + if line.is_downpayment and not line.display_type: + context = {'lang': line.order_partner_id.lang} + dp_state = line._get_downpayment_state() + if dp_state == 'draft': + name = _("%(line_description)s (Draft)", line_description=name) + elif dp_state == 'cancel': + name = _("%(line_description)s (Canceled)", line_description=name) + del context + line.name = name def _get_sale_order_line_multiline_description_sale(self): """ Compute a default multiline description for this sales order line. @@ -654,6 +664,20 @@ class SaleOrderLine(models.Model): for so_line in lines_by_analytic: so_line.qty_delivered = mapping.get(so_line.id or so_line._origin.id, 0.0) + def _get_downpayment_state(self): + self.ensure_one() + + if self.display_type: + return '' + + invoice_lines = self._get_invoice_lines() + if all(line.parent_state == 'draft' for line in invoice_lines): + return 'draft' + if all(line.parent_state == 'cancel' for line in invoice_lines): + return 'cancel' + + return '' + def _get_delivered_quantity_by_analytic(self, additional_domain): """ Compute and write the delivered quantity of current SO lines, based on their related analytic lines. diff --git a/addons/sale/report/ir_actions_report_templates.xml b/addons/sale/report/ir_actions_report_templates.xml index b6b49bd20a0..f0dd04defa4 100644 --- a/addons/sale/report/ir_actions_report_templates.xml +++ b/addons/sale/report/ir_actions_report_templates.xml @@ -63,7 +63,8 @@ - + + @@ -86,7 +87,7 @@ - + @@ -107,7 +108,7 @@ - @@ -126,7 +127,7 @@ - + + - + @@ -519,7 +520,7 @@ - @@ -537,8 +538,7 @@ - -
+
Subtotal diff --git a/addons/sale/tests/test_sale_refund.py b/addons/sale/tests/test_sale_refund.py index d51495cead3..a20964bc885 100644 --- a/addons/sale/tests/test_sale_refund.py +++ b/addons/sale/tests/test_sale_refund.py @@ -344,7 +344,8 @@ class TestSaleRefund(TestSaleCommon): }) downpayment.create_invoices() sale_order_refund.invoice_ids[0].action_post() - sol_downpayment = sale_order_refund.order_line[1] + # order_line[1] is the down payment section + sol_downpayment = sale_order_refund.order_line[2] payment = self.env['sale.advance.payment.inv'].with_context(so_context).create({ 'deposit_account_id': self.company_data['default_account_revenue'].id @@ -352,7 +353,8 @@ class TestSaleRefund(TestSaleCommon): payment.create_invoices() so_invoice = max(sale_order_refund.invoice_ids) - self.assertEqual(len(so_invoice.invoice_line_ids.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), len(sale_order_refund.order_line), 'All lines should be invoiced') + self.assertEqual(len(so_invoice.invoice_line_ids.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), + len(sale_order_refund.order_line.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), 'All lines should be invoiced') self.assertEqual(len(so_invoice.invoice_line_ids.filtered(lambda l: l.display_type == 'line_section' and l.name == "Down Payments")), 1, 'A single section for downpayments should be present') self.assertEqual(so_invoice.amount_total, sale_order_refund.amount_total - sol_downpayment.price_unit, 'Downpayment should be applied') so_invoice.action_post() diff --git a/addons/sale/tests/test_sale_to_invoice.py b/addons/sale/tests/test_sale_to_invoice.py index 7072ef85587..be7c6d2d29b 100644 --- a/addons/sale/tests/test_sale_to_invoice.py +++ b/addons/sale/tests/test_sale_to_invoice.py @@ -108,7 +108,7 @@ class TestSaleToInvoice(TestSaleCommon): self._check_order_search(self.sale_order, [('invoice_ids', '=', False)], self.env['sale.order']) self.assertEqual(len(self.sale_order.invoice_ids), 2, 'Invoice should be created for the SO') - downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment) + downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment and not l.display_type) self.assertEqual(len(downpayment_line), 2, 'SO line downpayment should be created on SO') # Update delivered quantity of SO lines @@ -124,7 +124,8 @@ class TestSaleToInvoice(TestSaleCommon): self.assertEqual(len(self.sale_order.invoice_ids), 3, 'Invoice should be created for the SO') invoice = max(self.sale_order.invoice_ids) - self.assertEqual(len(invoice.invoice_line_ids.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), len(self.sale_order.order_line), 'All lines should be invoiced') + self.assertEqual(len(invoice.invoice_line_ids.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), + len(self.sale_order.order_line.filtered(lambda l: not (l.display_type == 'line_section' and l.name == "Down Payments"))), 'All lines should be invoiced') self.assertEqual(len(invoice.invoice_line_ids.filtered(lambda l: l.display_type == 'line_section' and l.name == "Down Payments")), 1, 'A single section for downpayments should be present') self.assertEqual(invoice.amount_total, self.sale_order.amount_total - sum(downpayment_line.mapped('price_unit')), 'Downpayment should be applied') @@ -147,7 +148,7 @@ class TestSaleToInvoice(TestSaleCommon): payment.create_invoices() self.assertEqual(len(self.sale_order.invoice_ids), 1, 'Invoice should be created for the SO') - downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment) + downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment and not l.display_type) self.assertEqual(len(downpayment_line), 1, 'SO line downpayment should be created on SO') self.assertEqual(downpayment_line.price_unit, self.sale_order.amount_total/2, 'downpayment should have the correct amount') diff --git a/addons/sale/views/sale_order_views.xml b/addons/sale/views/sale_order_views.xml index 4230545c5bc..bf87909c5b3 100644 --- a/addons/sale/views/sale_order_views.xml +++ b/addons/sale/views/sale_order_views.xml @@ -503,8 +503,9 @@ optional="show" /> - - + + + diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml index 74c76831419..8028446d8cb 100644 --- a/addons/sale/views/sale_portal_templates.xml +++ b/addons/sale/views/sale_portal_templates.xml @@ -483,8 +483,9 @@
+
Subtotal diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index 284bbfd03d9..04eb414df79 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -136,10 +136,25 @@ class SaleAdvancePaymentInv(models.TransientModel): subtype_id=self.env.ref('mail.mt_note').id) return invoice + def _prepare_downpayment_section(self, order): + context = {'lang': order.partner_id.lang} + + so_values = { + 'name': _('Down Payments'), + 'product_uom_qty': 0.0, + 'order_id': order.id, + 'display_type': 'line_section', + 'is_downpayment': True, + 'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10, + } + + del context + return so_values + def _prepare_so_line(self, order, analytic_tag_ids, tax_ids, amount): context = {'lang': order.partner_id.lang} so_values = { - 'name': _('Down Payment: %s') % (time.strftime('%m %Y'),), + 'name': _('Down Payment: %s (Draft)') % (time.strftime('%m %Y'),), 'price_unit': amount, 'product_uom_qty': 0.0, 'order_id': order.id, @@ -179,6 +194,11 @@ class SaleAdvancePaymentInv(models.TransientModel): for line in order.order_line: analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in line.analytic_tag_ids] + # verify that no down payment section already exists + if not any(line.display_type and line.is_downpayment for line in order.order_line): + so_downpayment_section_values = self._prepare_downpayment_section(order) + sale_line_obj.create(so_downpayment_section_values) + so_line_values = self._prepare_so_line(order, analytic_tag_ids, tax_ids, amount) so_line = sale_line_obj.create(so_line_values) invoices += self._create_invoice(order, so_line, amount)