# Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) import odoo from odoo import fields from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet @odoo.tests.tagged("post_install", "-at_install") class TestRounded(TestCommonSaleTimesheet): @classmethod def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls.sale_order = cls.env["sale.order"].create( { "analytic_account_id": cls.project_global.analytic_account_id.id, "partner_id": cls.partner_a.id, "partner_invoice_id": cls.partner_a.id, "partner_shipping_id": cls.partner_a.id, } ) cls.env["sale.order.line"].create( { "order_id": cls.sale_order.id, "name": cls.product_delivery_timesheet2.name, "product_id": cls.product_delivery_timesheet2.id, "product_uom_qty": 1, "product_uom": cls.product_delivery_timesheet2.uom_id.id, "price_unit": cls.product_delivery_timesheet2.list_price, } ) cls.sale_order.action_confirm() cls.project_global.write( { "timesheet_rounding_unit": 0.25, "timesheet_rounding_method": "UP", "timesheet_rounding_factor": 200, } ) cls.product_expense = cls.env["product.product"].create( { "name": "Service delivered, EXPENSE", "expense_policy": "cost", "standard_price": 30, "list_price": 90, "type": "service", "invoice_policy": "order", "uom_id": cls.product_delivery_timesheet2.uom_id.id, "uom_po_id": cls.product_delivery_timesheet2.uom_id.id, } ) cls.analytic_plan = cls.env["account.analytic.plan"].create( { "name": "Plan sale timesheet", "company_id": False, } ) cls.avg_analytic_account = cls.env["account.analytic.account"].create( { "name": "AVG account", "plan_id": cls.analytic_plan.id, } ) def create_analytic_line(self, **kw): task = self.sale_order.tasks_ids[0] values = { "project_id": self.project_global.id, "task_id": task.id, "name": "Rounded test line", "date": fields.Date.today(), "unit_amount": 0, "product_id": self.product_delivery_timesheet2.id, "employee_id": self.employee_user.id, } values.update(kw) return self.env["account.analytic.line"].create(values) def test_analytic_line_init_no_rounding(self): lines = self.env["account.analytic.line"].search([]) for line in lines: self.assertEqual(line.unit_amount_rounded, line.unit_amount) def test_analytic_line_create_no_rounding(self): self.project_global.write({"timesheet_rounding_method": "NO"}) # no rounding enabled line = self.create_analytic_line(unit_amount=1) self.assertEqual(line.unit_amount, 1.0) self.assertEqual(line.unit_amount_rounded, line.unit_amount) def test_analytic_line_create(self): line = self.create_analytic_line(unit_amount=1) self.assertEqual(line.unit_amount_rounded, 2.0) line = self.create_analytic_line(unit_amount=1, unit_amount_rounded=0) self.assertEqual(line.unit_amount_rounded, 0.0) def test_analytic_line_create_and_update_amount_rounded(self): line = self.create_analytic_line(unit_amount=2) self.assertEqual(line.unit_amount_rounded, 4.0) line.write({"unit_amount_rounded": 5.0}) self.assertEqual(line.unit_amount_rounded, 5.0) line.write({"unit_amount_rounded": 0.0}) self.assertEqual(line.unit_amount_rounded, 0.0) def test_analytic_line_create_and_update_amount(self): line = self.create_analytic_line(unit_amount=2) self.assertEqual(line.unit_amount_rounded, 4.0) line.unit_amount = 5.0 self.assertEqual(line.unit_amount_rounded, 10.0) def test_analytic_line_read_group_override(self): # Test of the read group with an without timesheet_rounding context # without context the unit_amount should be the initial # with the context the value of unit_amount should be replaced by the # unit_amount_rounded line = self.env["account.analytic.line"] self.create_analytic_line(unit_amount=1) domain = [("project_id", "=", self.project_global.id)] fields_list = ["so_line", "unit_amount", "product_uom_id"] groupby = ["product_uom_id", "so_line"] data_ctx_f = line.read_group( domain, fields_list, groupby, ) self.assertEqual(data_ctx_f[0]["unit_amount"], 1.0) data_ctx_t = line.with_context(timesheet_rounding=True).read_group( domain, fields_list, groupby, ) self.assertEqual(data_ctx_t[0]["unit_amount"], 2.0) self.create_analytic_line(unit_amount=1.1) data_ctx_f = line.with_context(timesheet_rounding=False).read_group( domain, fields_list, groupby, ) self.assertEqual(data_ctx_f[0]["unit_amount"], 2.1) data_ctx_f = line.with_context(timesheet_rounding=True).read_group( domain, fields_list, groupby, ) self.assertEqual(data_ctx_f[0]["unit_amount"], 4.25) def test_analytic_line_read_override(self): # Cases for not rounding: # * not linked to project -> no impact # * is an expense -> no impact # * ctx key for rounding is set to false -> no impact # In all the other cases we check that unit amount is rounded. load = "_classic_read" fields = None # context = False + project_id - product_expense line = self.create_analytic_line(unit_amount=1) unit_amount_ret = line.read(fields, load)[0]["unit_amount"] self.assertEqual(unit_amount_ret, 1) # context = True + project_id + product_expense line = self.create_analytic_line( unit_amount=1, product_id=self.product_expense.id ) unit_amount_ret = line.with_context(timesheet_rounding=True).read(fields, load)[ 0 ]["unit_amount"] self.assertEqual(unit_amount_ret, 2) # context = True + project_id - product_expense line = self.create_analytic_line(unit_amount=1) unit_amount_ret = line.with_context(timesheet_rounding=True).read(fields, load)[ 0 ]["unit_amount"] self.assertEqual(unit_amount_ret, 2) def test_sale_order_qty_1(self): # amount=1 -> should be rounded to 2 by the invoicing_factor self.create_analytic_line(unit_amount=1) self.assertAlmostEqual(self.sale_order.order_line.qty_delivered, 2.0) self.assertAlmostEqual(self.sale_order.order_line.qty_to_invoice, 2.0) self.assertAlmostEqual(self.sale_order.order_line.qty_invoiced, 0) def test_sale_order_qty_2(self): # force amount_rounded=4 self.create_analytic_line(unit_amount=1, unit_amount_rounded=4) self.assertAlmostEqual(self.sale_order.order_line.qty_delivered, 4.0) self.assertAlmostEqual(self.sale_order.order_line.qty_to_invoice, 4.0) self.assertAlmostEqual(self.sale_order.order_line.qty_invoiced, 0) def test_sale_order_qty_3(self): # amount=0.9 # should be rounded to 2 by the invoicing_factor with the project # timesheet_rounding_unit: 0.25 # timesheet_rounding_method: 'UP' # timesheet_rounding_factor: 200 self.create_analytic_line(unit_amount=0.9) self.assertAlmostEqual(self.sale_order.order_line.qty_delivered, 2.0) self.assertAlmostEqual(self.sale_order.order_line.qty_to_invoice, 2.0) self.assertAlmostEqual(self.sale_order.order_line.qty_invoiced, 0) def test_sale_order_qty_4(self): # amount=0.9 # should be rounded to 2 by the invoicing_factor with the project # timesheet_rounding_unit: 0.25 # timesheet_rounding_method: 'UP' # timesheet_rounding_factor: 200 self.project_global.timesheet_rounding_factor = 400 self.create_analytic_line(unit_amount=1.0) self.assertAlmostEqual(self.sale_order.order_line.qty_delivered, 4.0) self.assertAlmostEqual(self.sale_order.order_line.qty_to_invoice, 4.0) self.assertAlmostEqual(self.sale_order.order_line.qty_invoiced, 0) def test_calc_rounded_amount_method(self): aal = self.env["account.analytic.line"] rounding_unit = 0.25 rounding_method = "UP" factor = 200 amount = 1 self.assertEqual( aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 ) rounding_unit = 0.0 rounding_method = "UP" factor = 200 amount = 1 self.assertEqual( aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 ) rounding_unit = 0.25 rounding_method = "UP" factor = 100 amount = 1.0 self.assertEqual( aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 1 ) rounding_unit = 0.25 rounding_method = "UP" factor = 200 amount = 0.9 self.assertEqual( aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 ) rounding_unit = 1.0 rounding_method = "UP" factor = 200 amount = 0.6 self.assertEqual( aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 ) rounding_unit = 0.25 rounding_method = "HALF_UP" factor = 200 amount = 1.01 self.assertEqual( aal._calc_rounded_amount(rounding_unit, rounding_method, factor, amount), 2 ) def test_post_invoice_with_rounded_amount_unchanged(self): """Posting an invoice MUST NOT recompute rounded amount unit. - invoicing the SO should not recompute and update the unit_amount_rounded - the invoiced qty should be the same as the aal.unit_amount_rounded """ unit_amount_rounded = 111 analytic_line = self.create_analytic_line(unit_amount=10) analytic_line.unit_amount_rounded = unit_amount_rounded account_move = self.sale_order._create_invoices() prd_ts_id = self.product_delivery_timesheet2 account_move._post() # the unit_amount_rounded is not changed self.assertEqual(analytic_line.unit_amount_rounded, unit_amount_rounded) # the invoiced qty remains the same inv_line = account_move.line_ids.filtered(lambda l: l.product_id == prd_ts_id) self.assertEqual(inv_line.quantity, unit_amount_rounded) def test_draft_invoice_with_rounded_amount_unchanged(self): """Drafting an invoice MUST NOT recompute rounded amount unit. - invoicing the SO should not recompute and update the unit_amount_rounded - the invoiced qty should be the same as the aal.unit_amount_rounded """ unit_amount_rounded = 0.12 analytic_line = self.create_analytic_line(unit_amount=10) analytic_line.unit_amount_rounded = unit_amount_rounded account_move = self.sale_order._create_invoices() prd_ts_id = self.product_delivery_timesheet2 account_move.button_draft() # the unit_amount_rounded is not changed self.assertEqual(analytic_line.unit_amount_rounded, unit_amount_rounded) # the invoiced qty remains the same inv_line = account_move.line_ids.filtered(lambda l: l.product_id == prd_ts_id) self.assertEqual(inv_line.quantity, unit_amount_rounded) def test_cancel_invoice_with_rounded_amount_unchanged(self): """Cancelling an invoice MUST NOT recompute rounded amount unit. - invoicing the SO should not recompute and update the unit_amount_rounded - the invoiced qty should be the same as the aal.unit_amount_rounded """ unit_amount_rounded_total = 15 analytic_line_1 = self.create_analytic_line(unit_amount=10) analytic_line_2 = self.create_analytic_line(unit_amount=10) analytic_line_1.unit_amount_rounded = unit_amount_rounded_total analytic_line_2.unit_amount_rounded = 0 account_move = self.sale_order._create_invoices() prd_ts_id = self.product_delivery_timesheet2 account_move.button_cancel() # the unit_amount_rounded is not changed self.assertEqual(analytic_line_1.unit_amount_rounded, unit_amount_rounded_total) self.assertEqual(analytic_line_2.unit_amount_rounded, 0) # the invoiced qty remains the same inv_line = account_move.line_ids.filtered(lambda l: l.product_id == prd_ts_id) self.assertEqual(inv_line.quantity, unit_amount_rounded_total)