From a11f402d6b949fdef8a86b532642287609ea74bc Mon Sep 17 00:00:00 2001 From: jbw-odoo Date: Mon, 16 Jan 2023 08:37:03 +0000 Subject: [PATCH] [IMP] sale: down payment tax breakdown Break down payments down per tax. When creating a down payment for a sale order, create one line per tax instead of one line for the enire order. [Sale Order] Product Qty Unit price Tax Subtotal Product1 1 1000 10% 1000 Product2 1 1000 5% 1000 ==== Untaxed 2000 Tax 150 ==== Total 2150 Make a 50% (1000) down payment : [BEFORE] Product Qty Unit price Tax Subtotal Product1 1 1000 10% 1000 Product2 1 1000 5% 1000 Down pay 1 1075 0 ==== Untaxed 2000 Tax 150 ==== Total 2150 [AFTER] Product Qty Unit price Tax Subtotal Product1 1 1000 10% 1000 Product2 1 1000 5% 1000 Down pay 1 500 10% 0 Down pay 1 500 5% 0 ==== Untaxed 2000 Tax 150 ==== Total 2150 closes odoo/odoo#105177 Task: 2678531 Related: odoo/enterprise#36331 Signed-off-by: Laurent Smet --- addons/sale/models/sale_order.py | 5 +- addons/sale/models/sale_order_line.py | 3 +- addons/sale/tests/__init__.py | 1 + .../tests/test_sale_order_down_payment.py | 405 ++++++++++++++++++ addons/sale/views/sale_order_views.xml | 8 +- .../sale/wizard/sale_make_invoice_advance.py | 132 ++++-- 6 files changed, 512 insertions(+), 42 deletions(-) create mode 100644 addons/sale/tests/test_sale_order_down_payment.py diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index ddcede255c6..3607b0c47d8 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -1022,8 +1022,9 @@ class SaleOrder(models.Model): 'invoice_line_ids': [], } - def action_view_invoice(self): - invoices = self.mapped('invoice_ids') + def action_view_invoice(self, invoices=False): + if not invoices: + invoices = self.mapped('invoice_ids') action = self.env['ir.actions.actions']._for_xml_id('account.action_move_out_invoice_type') if len(invoices) > 1: action['domain'] = [('id', 'in', invoices.ids)] diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 21eb4bee9bf..57ffa16b05c 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -572,7 +572,7 @@ class SaleOrderLine(models.Model): # otherwise it's a surcharge which shouldn't be shown to the customer line.discount = discount - def _convert_to_tax_base_line_dict(self): + def _convert_to_tax_base_line_dict(self, **kwargs): """ Convert the current record to a dictionary in order to use the generic taxes computation method defined on account.tax. @@ -589,6 +589,7 @@ class SaleOrderLine(models.Model): quantity=self.product_uom_qty, discount=self.discount, price_subtotal=self.price_subtotal, + **kwargs, ) @api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id') diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py index ed2b40060bd..9348f7d6e16 100644 --- a/addons/sale/tests/__init__.py +++ b/addons/sale/tests/__init__.py @@ -16,3 +16,4 @@ from . import test_sale_product_attribute_value_config from . import test_sale_refund from . import test_sale_tax_totals from . import test_sale_to_invoice +from . import test_sale_order_down_payment diff --git a/addons/sale/tests/test_sale_order_down_payment.py b/addons/sale/tests/test_sale_order_down_payment.py new file mode 100644 index 00000000000..e23aec7c2e5 --- /dev/null +++ b/addons/sale/tests/test_sale_order_down_payment.py @@ -0,0 +1,405 @@ +from odoo.tests import tagged +from odoo import Command +from .common import TestSaleCommon + + +@tagged('post_install', '-at_install') +class TestSaleOrderDownPayment(TestSaleCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + SaleOrder = cls.env['sale.order'].with_context(tracking_disable=True) + + cls.tax_10 = cls.create_tax(10) + cls.tax_15 = cls.create_tax(15) + + # create a generic Sale Order with all classical products and empty pricelist + cls.sale_order = SaleOrder.create({ + 'partner_id': cls.partner_a.id, + 'partner_invoice_id': cls.partner_a.id, + 'partner_shipping_id': cls.partner_a.id, + 'pricelist_id': cls.company_data['default_pricelist'].id, + }) + cls.sol_product_order = cls.env['sale.order.line'].create({ + 'name': cls.company_data['product_order_no'].name, + 'product_id': cls.company_data['product_order_no'].id, + 'product_uom_qty': 2, + 'product_uom': cls.company_data['product_order_no'].uom_id.id, + 'price_unit': 100, + 'order_id': cls.sale_order.id, + 'tax_id': False, + }) + cls.sol_serv_deliver = cls.env['sale.order.line'].create({ + 'name': cls.company_data['product_service_delivery'].name, + 'product_id': cls.company_data['product_service_delivery'].id, + 'product_uom_qty': 2, + 'product_uom': cls.company_data['product_service_delivery'].uom_id.id, + 'price_unit': 100, + 'order_id': cls.sale_order.id, + 'tax_id': False, + }) + cls.sol_serv_order = cls.env['sale.order.line'].create({ + 'name': cls.company_data['product_service_order'].name, + 'product_id': cls.company_data['product_service_order'].id, + 'product_uom_qty': 2, + 'product_uom': cls.company_data['product_service_order'].uom_id.id, + 'price_unit': 100, + 'order_id': cls.sale_order.id, + 'tax_id': False, + }) + cls.sol_product_deliver = cls.env['sale.order.line'].create({ + 'name': cls.company_data['product_delivery_no'].name, + 'product_id': cls.company_data['product_delivery_no'].id, + 'product_uom_qty': 2, + 'product_uom': cls.company_data['product_delivery_no'].uom_id.id, + 'price_unit': 100, + 'order_id': cls.sale_order.id, + 'tax_id': False, + }) + + cls.revenue_account = cls.company_data['default_account_revenue'] + cls.receivable_account = cls.company_data['default_account_receivable'] + + @classmethod + def create_tax(cls, amount, values=None): + vals = { + 'name': 'Tax %s' % amount, + 'amount_type': 'percent', + 'amount': amount, + 'type_tax_use': 'sale', + } + if values: + vals.update(values) + return cls.env['account.tax'].create(vals) + + @classmethod + def make_downpayment(cls, **kwargs): + so_context = { + 'active_model': 'sale.order', + 'active_ids': [cls.sale_order.id], + 'active_id': cls.sale_order.id, + 'default_journal_id': cls.company_data['default_journal_sale'].id, + } + payment_params = { + 'advance_payment_method': 'percentage', + 'amount': 50, + 'deposit_account_id': cls.revenue_account.id, + **kwargs, + } + downpayment = cls.env['sale.advance.payment.inv'].with_context(so_context).create(payment_params) + downpayment.create_invoices() + cls.sale_order.action_confirm() + + def _assert_invoice_lines_values(self, lines, expected): + return self.assertRecordValues(lines, [dict(zip(expected[0], x)) for x in expected[1:]]) + + def test_tax_breakdown(self): + self.sale_order.order_line[0].tax_id = self.tax_15 + self.tax_10 + self.sale_order.order_line[1].tax_id = self.tax_10 + self.sale_order.order_line[2].tax_id = self.tax_10 + self.make_downpayment() + invoice = self.sale_order.invoice_ids + down_pay_amt = self.sale_order.amount_total / 2 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total'], + # base lines + [self.revenue_account.id, (self.tax_15 + self.tax_10).ids, -100, 125 ], + [self.revenue_account.id, self.tax_10.ids, -200, 220 ], + [self.revenue_account.id, self.env['account.tax'], -100, 100 ], + # taxes + [self.revenue_account.id, self.env['account.tax'], -30, 0 ], + [self.revenue_account.id, self.env['account.tax'], -15, 0 ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0 ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_tax_breakdown_other_currency(self): + self.sale_order.currency_id = self.currency_data['currency'] # rate = 2.0 + self.sale_order.order_line[0].tax_id = self.tax_15 + self.tax_10 + self.sale_order.order_line[1].tax_id = self.tax_10 + self.sale_order.order_line[2].tax_id = self.tax_10 + self.make_downpayment() + invoice = self.sale_order.invoice_ids + down_pay_amt = self.sale_order.amount_total / 2 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total'], + # base lines + [self.revenue_account.id, (self.tax_15 + self.tax_10).ids, -50, 125 ], + [self.revenue_account.id, self.tax_10.ids, -100, 220 ], + [self.revenue_account.id, self.env['account.tax'], -50, 100 ], + # taxes + [self.revenue_account.id, self.env['account.tax'], -15, 0 ], + [self.revenue_account.id, self.env['account.tax'], -7.5, 0 ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt / 2.0, 0 ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_tax_breakdown_fixed_payment_method(self): + self.sale_order.order_line[0].tax_id = self.tax_15 + self.tax_10 + self.sale_order.order_line[1].tax_id = self.tax_10 + self.sale_order.order_line[2].tax_id = self.tax_10 + self.make_downpayment(advance_payment_method='fixed', fixed_amount=222.5, amount=0) + invoice = self.sale_order.invoice_ids + down_pay_amt = 222.5 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total'], + # base lines + [self.revenue_account.id, (self.tax_15 + self.tax_10).ids, -50, 62.5 ], + [self.revenue_account.id, self.tax_10.ids, -100, 110 ], + [self.revenue_account.id, self.env['account.tax'], -50, 50 ], + # taxes + [self.revenue_account.id, self.env['account.tax'], -15, 0 ], + [self.revenue_account.id, self.env['account.tax'], -7.5, 0 ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0 ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_tax_breakdown_fixed_payment_method_with_taxes_on_all_lines(self): + self.sale_order.order_line[0].tax_id = self.tax_15 + self.sale_order.order_line[1].tax_id = self.tax_10 + self.sale_order.order_line[2].tax_id = self.tax_10 + self.sale_order.order_line[3].tax_id = self.tax_10 + self.make_downpayment(advance_payment_method='fixed', fixed_amount=222.5, amount=0) + invoice = self.sale_order.invoice_ids + down_pay_amt = 222.5 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total'], + # base lines + [self.revenue_account.id, self.tax_15.ids, -50, 57.5 ], + [self.revenue_account.id, self.tax_10.ids, -150, 165 ], + # taxes + [self.revenue_account.id, self.env['account.tax'], -7.5, 0 ], + [self.revenue_account.id, self.env['account.tax'], -15, 0 ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0 ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_tax_price_include_breakdown(self): + tax_10_incl = self.create_tax(10, {'price_include': True}) + self.sale_order.order_line[0].tax_id = tax_10_incl + self.tax_10 + self.sale_order.order_line[1].tax_id = self.tax_10 + self.sale_order.order_line[2].tax_id = self.tax_10 + self.make_downpayment() + invoice = self.sale_order.invoice_ids + down_pay_amt = self.sale_order.amount_total / 2 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total'], + # base lines + [self.revenue_account.id, (tax_10_incl + self.tax_10).ids, -90.91, 109.09 ], + [self.revenue_account.id, self.tax_10.ids, -200, 220 ], + [self.revenue_account.id, self.env['account.tax'], -100, 100 ], + # taxes + [self.revenue_account.id, self.env['account.tax'], -29.09, 0 ], + [self.revenue_account.id, self.env['account.tax'], -9.09, 0 ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0 ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_tax_price_include_include_base_amount_breakdown(self): + tax_10_pi_ba = self.create_tax(10, {'price_include': True, 'include_base_amount': True}) + self.tax_10.sequence = 2 + self.sale_order.order_line[0].tax_id = tax_10_pi_ba + self.tax_10 + self.sale_order.order_line[1].tax_id = self.tax_10 + self.sale_order.order_line[2].tax_id = self.tax_10 + self.make_downpayment() + invoice = self.sale_order.invoice_ids + down_pay_amt = self.sale_order.amount_total / 2 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total'], + # base lines + [self.revenue_account.id, (tax_10_pi_ba + self.tax_10).ids, -90.91, 110 ], + [self.revenue_account.id, self.tax_10.ids, -200, 220 ], + [self.revenue_account.id, self.env['account.tax'], -100, 100 ], + # taxes + [self.revenue_account.id, self.tax_10.ids, -9.09, 0 ], + [self.revenue_account.id, self.env['account.tax'], -30, 0 ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0 ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_tax_breakdown_with_discount(self): + self.sale_order.order_line[0].tax_id = self.tax_10 + self.sale_order.order_line[1].tax_id = self.tax_10 + self.sale_order.order_line[1].discount = 25.0 + self.sale_order.order_line[2].tax_id = self.tax_15 + self.make_downpayment() + invoice = self.sale_order.invoice_ids + down_pay_amt = self.sale_order.amount_total / 2 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total' ], + # base lines + [self.revenue_account.id, self.tax_10.ids, -175, 192.5 ], + [self.revenue_account.id, self.tax_15.ids, -100, 115 ], + [self.revenue_account.id, self.env['account.tax'], -100, 100 ], + # taxes + [self.revenue_account.id, self.env['account.tax'], -17.5, 0 ], + [self.revenue_account.id, self.env['account.tax'], -15, 0 ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0 ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_tax_price_include_include_base_amount_breakdown_with_discount(self): + tax_10_pi_ba = self.create_tax(10, {'price_include': True, 'include_base_amount': True}) + self.tax_10.sequence = 2 + self.sale_order.order_line[0].tax_id = tax_10_pi_ba + self.tax_10 + self.sale_order.order_line[0].discount = 25.0 + self.sale_order.order_line[1].tax_id = self.tax_10 + self.sale_order.order_line[2].tax_id = self.tax_10 + self.make_downpayment() + invoice = self.sale_order.invoice_ids + down_pay_amt = self.sale_order.amount_total / 2 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total'], + # base lines + [self.revenue_account.id, (tax_10_pi_ba + self.tax_10).ids, -68.18, 82.5 ], + [self.revenue_account.id, self.tax_10.ids, -200, 220 ], + [self.revenue_account.id, self.env['account.tax'], -100, 100 ], + # taxes + [self.revenue_account.id, self.tax_10.ids, -6.82, 0 ], + [self.revenue_account.id, self.env['account.tax'], -27.5, 0 ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0 ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_tax_fixed_amount_breakdown(self): + tax_10_fix_a = self.create_tax(10, {'amount_type': 'fixed', 'include_base_amount': True}) + tax_10_fix_b = self.create_tax(10, {'amount_type': 'fixed', 'include_base_amount': True}) + tax_10_fix_c = self.create_tax(10, {'amount_type': 'fixed'}) + tax_10_a = self.tax_10 + tax_10_b = self.create_tax(10) + tax_group_1 = self.env['account.tax'].create({ + 'name': "Tax Group", + 'amount_type': 'group', + 'children_tax_ids': [Command.set((tax_10_fix_a + tax_10_a + tax_10_fix_b + tax_10_b).ids)], + 'type_tax_use': 'sale', + }) + tax_group_2 = self.env['account.tax'].create({ + 'name': "Tax Group 2", + 'amount_type': 'group', + 'children_tax_ids': [Command.set((tax_10_fix_c + tax_10_a).ids)], + 'type_tax_use': 'sale', + }) + self.sale_order.order_line[0].tax_id = tax_group_1 + self.sale_order.order_line[1].tax_id = tax_group_2 + self.sale_order.order_line[2].tax_id = tax_10_a + self.make_downpayment() + invoice = self.sale_order.invoice_ids + down_pay_amt = self.sale_order.amount_total / 2 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total'], + # base lines + [self.revenue_account.id, (tax_10_a + tax_10_b).ids, -110, 132 ], + [self.revenue_account.id, tax_10_b.ids, -10, 11 ], + [self.revenue_account.id, tax_10_a.ids, -200, 220 ], + [self.revenue_account.id, self.env['account.tax'], -110, 110 ], + # taxes + [self.revenue_account.id, self.env['account.tax'], -31, 0 ], + [self.revenue_account.id, self.env['account.tax'], -12, 0 ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0 ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_analytic_distribution(self): + analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False}) + an_acc_01 = str(self.env['account.analytic.account'].create({'name': 'Account 01', 'plan_id': analytic_plan.id}).id) + an_acc_02 = str(self.env['account.analytic.account'].create({'name': 'Account 02', 'plan_id': analytic_plan.id}).id) + self.sale_order.order_line[0].tax_id = self.tax_15 + self.tax_10 + self.sale_order.order_line[0].analytic_distribution = {an_acc_01: 100} + self.sale_order.order_line[1].tax_id = self.tax_10 + self.sale_order.order_line[1].analytic_distribution = {an_acc_01: 50, an_acc_02: 50} + self.sale_order.order_line[2].tax_id = self.tax_10 + self.sale_order.order_line[2].analytic_distribution = {an_acc_01: 100} + self.make_downpayment() + invoice = self.sale_order.invoice_ids + down_pay_amt = self.sale_order.amount_total / 2 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total', 'analytic_distribution' ], + # base lines + [self.revenue_account.id, (self.tax_15 + self.tax_10).ids, -100, 125, {an_acc_01: 100} ], + [self.revenue_account.id, self.tax_10.ids, -100, 110, {an_acc_01: 50, an_acc_02: 50}], + [self.revenue_account.id, self.tax_10.ids, -100, 110, {an_acc_01: 100}], + [self.revenue_account.id, self.env['account.tax'], -100, 100 , False ], + # taxes + [self.revenue_account.id, self.env['account.tax'], -30, 0, False ], + [self.revenue_account.id, self.env['account.tax'], -15, 0, False ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0, False ], + ] + + self._assert_invoice_lines_values(invoice.line_ids, expected) + + def test_tax_fixed_amount_analytic_distribution(self): + analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False}) + an_acc_01 = str(self.env['account.analytic.account'].create({'name': 'Account 01', 'plan_id': analytic_plan.id}).id) + an_acc_02 = str(self.env['account.analytic.account'].create({'name': 'Account 02', 'plan_id': analytic_plan.id}).id) + tax_10_fix_a = self.create_tax(10, {'amount_type': 'fixed', 'include_base_amount': True}) + tax_10_fix_b = self.create_tax(10, {'amount_type': 'fixed', 'include_base_amount': True}) + tax_10_fix_c = self.create_tax(10, {'amount_type': 'fixed'}) + tax_10_a = self.tax_10 + tax_10_b = self.create_tax(10) + tax_group_1 = self.env['account.tax'].create({ + 'name': "Tax Group", + 'amount_type': 'group', + 'children_tax_ids': [Command.set((tax_10_fix_a + tax_10_a + tax_10_fix_b + tax_10_b).ids)], + 'type_tax_use': 'sale', + }) + tax_group_2 = self.env['account.tax'].create({ + 'name': "Tax Group 2", + 'amount_type': 'group', + 'children_tax_ids': [Command.set((tax_10_fix_c + tax_10_a).ids)], + 'type_tax_use': 'sale', + }) + self.sale_order.order_line[0].tax_id = tax_group_1 + self.sale_order.order_line[0].analytic_distribution = {an_acc_01: 50, an_acc_02: 50} + self.sale_order.order_line[1].tax_id = tax_group_2 + self.sale_order.order_line[2].tax_id = tax_10_a + self.make_downpayment() + invoice = self.sale_order.invoice_ids + down_pay_amt = self.sale_order.amount_total / 2 + # pylint: disable=C0326 + expected = [ + # keys + ['account_id', 'tax_ids', 'balance', 'price_total', 'analytic_distribution'], + # base lines + [self.revenue_account.id, (tax_10_a + tax_10_b).ids, -110, 132, {an_acc_01: 50, an_acc_02: 50}], + [self.revenue_account.id, tax_10_b.ids, -10, 11, {an_acc_01: 50, an_acc_02: 50}], + [self.revenue_account.id, tax_10_a.ids, -200, 220, False ], + [self.revenue_account.id, self.env['account.tax'], -110, 110, False ], + # taxes + [self.revenue_account.id, self.env['account.tax'], -31, 0, False ], + [self.revenue_account.id, self.env['account.tax'], -12, 0, False ], + # receivable + [self.receivable_account.id, self.env['account.tax'], down_pay_amt, 0, False ], + ] + self._assert_invoice_lines_values(invoice.line_ids, expected) diff --git a/addons/sale/views/sale_order_views.xml b/addons/sale/views/sale_order_views.xml index 334904f79d1..b41b5c6a2fb 100644 --- a/addons/sale/views/sale_order_views.xml +++ b/addons/sale/views/sale_order_views.xml @@ -403,6 +403,7 @@ + @@ -499,7 +501,7 @@ @@ -513,7 +515,7 @@ options="{'no_create': True}" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id), ('country_id', '=', parent.tax_country_id)]" context="{'active_test': True}" - attrs="{'readonly': [('qty_invoiced', '>', 0)]}" + attrs="{'readonly': ['|', ('qty_invoiced', '>', 0), ('is_downpayment', '==', True)]}" optional="show" /> diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index 7dc24060f5f..206041fd791 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -6,7 +6,7 @@ import time from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.fields import Command -from odoo.tools import float_is_zero +from odoo.tools import frozendict class SaleAdvancePaymentInv(models.TransientModel): @@ -153,10 +153,9 @@ class SaleAdvancePaymentInv(models.TransientModel): #=== ACTION METHODS ===# def create_invoices(self): - self._create_invoices(self.sale_order_ids) - + invoices = self._create_invoices(self.sale_order_ids) if self.env.context.get('open_invoices'): - return self.sale_order_ids.action_view_invoice() + return self.sale_order_ids.action_view_invoice(invoices=invoices) return {'type': 'ir.actions.act_window_close'} @@ -184,14 +183,32 @@ class SaleAdvancePaymentInv(models.TransientModel): self._prepare_down_payment_section_values(order) ) - down_payment_so_line = self.env['sale.order.line'].create( - self._prepare_so_line_values(order) + down_payment_lines = self.env['sale.order.line'].create( + self._prepare_down_payment_lines_values(order) ) invoice = self.env['account.move'].sudo().create( - self._prepare_invoice_values(order, down_payment_so_line) + self._prepare_invoice_values(order, down_payment_lines) ).with_user(self.env.uid) # Unsudo the invoice after creation + # Ensure the invoice total is exactly the expected fixed amount. + if self.advance_payment_method == 'fixed': + delta_amount = (invoice.amount_total - self.fixed_amount) * (1 if invoice.is_inbound() else -1) + if not order.currency_id.is_zero(delta_amount): + tax_line = invoice.line_ids\ + .filtered(lambda aml: aml.tax_line_id.amount_type not in (False, 'fixed'))[:1] + receivable_line = invoice.line_ids\ + .filtered(lambda aml: aml.account_id.account_type == 'asset_receivable')[:1] + if tax_line and receivable_line: + invoice.line_ids = [ + Command.update(tax_line.id, { + 'amount_currency': tax_line.amount_currency + delta_amount, + }), + Command.update(receivable_line.id, { + 'amount_currency': receivable_line.amount_currency + delta_amount, + }), + ] + invoice.message_post_with_source( 'mail.message_origin_link', render_values={'self': invoice, 'origin': order}, @@ -226,52 +243,96 @@ class SaleAdvancePaymentInv(models.TransientModel): del context return so_values - def _prepare_so_line_values(self, order): + def _prepare_down_payment_lines_values(self, order): + """ Create one down payment line per tax or unique taxes combination. + Apply the tax(es) to their respective lines. + + :param order: Order for which the down payment lines are created. + :return: An array of dicts with the down payment lines values. + """ + self.ensure_one() + + if self.advance_payment_method == 'percentage': + percentage = self.amount / 100 + else: + percentage = self.fixed_amount / order.amount_total + + order_lines = order.order_line.filtered(lambda l: not l.display_type) + base_downpayment_lines_values = self._prepare_base_downpayment_line_values(order) + + tax_base_line_dicts = [line._convert_to_tax_base_line_dict( + analytic_distribution=line.analytic_distribution) for line in order_lines] + computed_taxes = self.env['account.tax']._compute_taxes( + tax_base_line_dicts, + handle_price_include=False + ) + down_payment_values = [] + for line, tax_repartition in computed_taxes['base_lines_to_update']: + taxes = line['taxes'].flatten_taxes_hierarchy() + fixed_taxes = taxes.filtered(lambda tax: tax.amount_type == 'fixed') + down_payment_values.append([ + taxes - fixed_taxes, + line['analytic_distribution'], + tax_repartition['price_subtotal'] + ]) + for fixed_tax in fixed_taxes: + # Fixed taxes cannot be set as taxes on down payments as they always amounts to 100% + # of the tax amount. Therefore fixed taxes are removed and are replace by a new line + # with appropriate amount, and non fixed taxes if the fixed tax affected the base of + # any other non fixed tax. + if fixed_tax.include_base_amount: + pct_tax = taxes[list(taxes).index(fixed_tax) + 1:]\ + .filtered(lambda t: t.is_base_affected and t.amount_type != 'fixed') + else: + pct_tax = self.env['account.tax'] + down_payment_values.append([ + pct_tax, + line['analytic_distribution'], + line['quantity'] * fixed_tax.amount + ]) + + downpayment_line_map = {} + for tax_id, analytic_distribution, price_subtotal in down_payment_values: + grouping_key = frozendict({ + 'tax_id': tuple(sorted(tax_id.ids)), + 'analytic_distribution': analytic_distribution, + }) + downpayment_line_map.setdefault(grouping_key, { + **base_downpayment_lines_values, + **grouping_key, + 'product_uom_qty': 0.0, + 'price_unit': 0.0, + }) + downpayment_line_map[grouping_key]['price_unit'] += \ + order.currency_id.round(price_subtotal * percentage) + + return list(downpayment_line_map.values()) + + def _prepare_base_downpayment_line_values(self, order): self.ensure_one() - analytic_distribution = {} - amount_total = sum(order.order_line.mapped("price_total")) - if not float_is_zero(amount_total, precision_rounding=self.currency_id.rounding): - for line in order.order_line: - distrib_dict = line.analytic_distribution or {} - for account, distribution in distrib_dict.items(): - analytic_distribution[account] = distribution * line.price_total + analytic_distribution.get(account, 0) - for account, distribution_amount in analytic_distribution.items(): - analytic_distribution[account] = distribution_amount/amount_total context = {'lang': order.partner_id.lang} so_values = { 'name': _('Down Payment: %s (Draft)', time.strftime('%m %Y')), - 'price_unit': self._get_down_payment_amount(order), 'product_uom_qty': 0.0, 'order_id': order.id, 'discount': 0.0, 'product_id': self.product_id.id, - 'analytic_distribution': analytic_distribution, 'is_downpayment': True, 'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10, } del context return so_values - def _get_down_payment_amount(self, order): - self.ensure_one() - if self.advance_payment_method == 'percentage': - amount = order.amount_total * self.amount / 100 - else: # Fixed amount - amount = self.fixed_amount - return amount - - def _prepare_invoice_values(self, order, so_line): + def _prepare_invoice_values(self, order, so_lines): self.ensure_one() return { **order._prepare_invoice(), - 'invoice_line_ids': [ - Command.create( - so_line._prepare_invoice_line( - name=self._get_down_payment_description(order), - quantity=1.0, - ) + 'invoice_line_ids': [Command.create( + line._prepare_invoice_line( + name=self._get_down_payment_description(order), + quantity=1.0, ) - ], + ) for line in so_lines], } def _get_down_payment_description(self, order): @@ -282,5 +343,4 @@ class SaleAdvancePaymentInv(models.TransientModel): else: name = _('Down Payment') del context - return name