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)