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