diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index cd5067aed1c..904ce9573a1 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -374,7 +374,7 @@ class PosSession(models.Model): # in the self.order_ids with group key of the `payment_method_id` # field of the pos.payment record. amounts = lambda: {'amount': 0.0, 'amount_converted': 0.0} - tax_amounts = lambda: {'amount': 0.0, 'amount_converted': 0.0, 'base_amount': 0.0} + tax_amounts = lambda: {'amount': 0.0, 'amount_converted': 0.0, 'base_amount': 0.0, 'base_amount_converted': 0.0} split_receivables = defaultdict(amounts) split_receivables_cash = defaultdict(amounts) combine_receivables = defaultdict(amounts) @@ -457,8 +457,8 @@ class PosSession(models.Model): exp_key = move.product_id._get_product_accounts()['expense'] out_key = move.product_id.categ_id.property_stock_account_output_categ_id amount = -sum(move.stock_valuation_layer_ids.mapped('value')) - stock_expense[exp_key] = self._update_amounts(stock_expense[exp_key], {'amount': amount}, move.picking_id.date) - stock_output[out_key] = self._update_amounts(stock_output[out_key], {'amount': amount}, move.picking_id.date) + stock_expense[exp_key] = self._update_amounts(stock_expense[exp_key], {'amount': amount}, move.picking_id.date, force_company_currency=True) + stock_output[out_key] = self._update_amounts(stock_output[out_key], {'amount': amount}, move.picking_id.date, force_company_currency=True) if self.config_id.cash_rounding: diff = order.amount_paid - order.amount_total @@ -514,7 +514,7 @@ class PosSession(models.Model): rounding_difference = data.get('rounding_difference') MoveLine = data.get('MoveLine') - tax_vals = [self._get_tax_vals(key, amounts['amount'], amounts['amount_converted'], amounts['base_amount']) for key, amounts in taxes.items() if amounts['amount']] + tax_vals = [self._get_tax_vals(key, amounts['amount'], amounts['amount_converted'], amounts['base_amount_converted']) for key, amounts in taxes.items() if amounts['amount']] # Check if all taxes lines have account_id assigned. If not, there are repartition lines of the tax that have no account_id. tax_names_no_account = [line['name'] for line in tax_vals if line['account_id'] == False] if len(tax_names_no_account) > 0: @@ -746,14 +746,14 @@ class PosSession(models.Model): } return self._credit_amounts(partial_vals, amount, amount_converted) - def _get_tax_vals(self, key, amount, amount_converted, base_amount): + def _get_tax_vals(self, key, amount, amount_converted, base_amount_converted): account_id, repartition_line_id, tax_id, tag_ids = key tax = self.env['account.tax'].browse(tax_id) partial_args = { 'name': tax.name, 'account_id': account_id, 'move_id': self.move_id.id, - 'tax_base_amount': base_amount, + 'tax_base_amount': base_amount_converted, 'tax_repartition_line_id': repartition_line_id, 'tag_ids': [(6, 0, tag_ids)], } @@ -761,11 +761,11 @@ class PosSession(models.Model): def _get_stock_expense_vals(self, exp_account, amount, amount_converted): partial_args = {'account_id': exp_account.id, 'move_id': self.move_id.id} - return self._debit_amounts(partial_args, amount, amount_converted) + return self._debit_amounts(partial_args, amount, amount_converted, force_company_currency=True) def _get_stock_output_vals(self, out_account, amount, amount_converted): partial_args = {'account_id': out_account.id, 'move_id': self.move_id.id} - return self._credit_amounts(partial_args, amount, amount_converted) + return self._credit_amounts(partial_args, amount, amount_converted, force_company_currency=True) def _get_statement_line_vals(self, statement, receivable_account, amount): return { @@ -776,15 +776,67 @@ class PosSession(models.Model): 'account_id': receivable_account.id, } - def _update_amounts(self, old_amounts, amounts_to_add, date, round=True): - new_amounts = {} - for k, amount in old_amounts.items(): - if k == 'amount_converted': - amount_converted = old_amounts['amount_converted'] - amount_to_convert = amounts_to_add['amount'] - new_amounts['amount_converted'] = amount_converted if self.is_in_company_currency else (amount_converted + self._amount_converter(amount_to_convert, date, round)) + def _update_amounts(self, old_amounts, amounts_to_add, date, round=True, force_company_currency=False): + """Responsible for adding `amounts_to_add` to `old_amounts` considering the currency of the session. + + old_amounts { new_amounts { + amount amounts_to_add { amount + amount_converted + amount -> amount_converted + [base_amount [base_amount] [base_amount + base_amount_converted] } base_amount_converted] + } } + + NOTE: + - Notice that `amounts_to_add` does not have `amount_converted` field. + This function is responsible in calculating the `amount_converted` from the + `amount` of `amounts_to_add` which is used to update the values of `old_amounts`. + - Values of `amount` and/or `base_amount` should always be in session's currency [1]. + - Value of `amount_converted` should be in company's currency + + [1] Except when `force_company_currency` = True. It means that values in `amounts_to_add` + is in company currency. + + :params old_amounts dict: + Amounts to update + :params amounts_to_add dict: + Amounts used to update the old_amounts + :params date date: + Date used for conversion + :params round bool: + Same as round parameter of `res.currency._convert`. + Defaults to True because that is the default of `res.currency._convert`. + We put it to False if we want to round globally. + :params force_company_currency bool: + If True, the values in amounts_to_add are in company's currency. + Defaults to False because it is only used to anglo-saxon lines. + + :return dict: new amounts combining the values of `old_amounts` and `amounts_to_add`. + """ + # make a copy of the old amounts + new_amounts = { **old_amounts } + + amount = amounts_to_add.get('amount') + if self.is_in_company_currency or force_company_currency: + amount_converted = amount + else: + amount_converted = self._amount_converter(amount, date, round) + + # update amount and amount converted + new_amounts['amount'] += amount + new_amounts['amount_converted'] += amount_converted + + # consider base_amount if present + if not amounts_to_add.get('base_amount') == None: + base_amount = amounts_to_add.get('base_amount') + if self.is_in_company_currency or force_company_currency: + base_amount_converted = base_amount else: - new_amounts[k] = old_amounts[k] + amounts_to_add[k] + base_amount_converted = self._amount_converter(base_amount, date, round) + + # update base_amount and base_amount_converted + new_amounts['base_amount'] += base_amount + new_amounts['base_amount_converted'] += base_amount_converted + return new_amounts def _round_amounts(self, amounts): @@ -797,7 +849,7 @@ class PosSession(models.Model): new_amounts[key] = self.currency_id.round(amount) return new_amounts - def _credit_amounts(self, partial_move_line_vals, amount, amount_converted): + def _credit_amounts(self, partial_move_line_vals, amount, amount_converted, force_company_currency=False): """ `partial_move_line_vals` is completed by `credit`ing the given amounts. NOTE Amounts in PoS are in the currency of journal_id in the session.config_id. @@ -814,40 +866,38 @@ class PosSession(models.Model): :return dict: complete values for creating 'amount.move.line' record """ - if self.is_in_company_currency: - return { - 'debit': -amount if amount < 0.0 else 0.0, - 'credit': amount if amount > 0.0 else 0.0, - **partial_move_line_vals - } + if self.is_in_company_currency or force_company_currency: + additional_field = {} else: - return { - 'debit': -amount_converted if amount_converted < 0.0 else 0.0, - 'credit': amount_converted if amount_converted > 0.0 else 0.0, + additional_field = { 'amount_currency': -amount if amount_converted > 0 else amount, 'currency_id': self.currency_id.id, - **partial_move_line_vals } + return { + 'debit': -amount_converted if amount_converted < 0.0 else 0.0, + 'credit': amount_converted if amount_converted > 0.0 else 0.0, + **partial_move_line_vals, + **additional_field, + } - def _debit_amounts(self, partial_move_line_vals, amount, amount_converted): + def _debit_amounts(self, partial_move_line_vals, amount, amount_converted, force_company_currency=False): """ `partial_move_line_vals` is completed by `debit`ing the given amounts. See _credit_amounts docs for more details. """ - if self.is_in_company_currency: - return { - 'debit': amount if amount > 0.0 else 0.0, - 'credit': -amount if amount < 0.0 else 0.0, - **partial_move_line_vals - } + if self.is_in_company_currency or force_company_currency: + additional_field = {} else: - return { - 'debit': amount_converted if amount_converted > 0.0 else 0.0, - 'credit': -amount_converted if amount_converted < 0.0 else 0.0, + additional_field = { 'amount_currency': amount if amount_converted > 0 else -amount, 'currency_id': self.currency_id.id, - **partial_move_line_vals } + return { + 'debit': amount_converted if amount_converted > 0.0 else 0.0, + 'credit': -amount_converted if amount_converted < 0.0 else 0.0, + **partial_move_line_vals, + **additional_field, + } def _amount_converter(self, amount, date, round): # self should be single record as this method is only called in the subfunctions of self._validate_session diff --git a/addons/point_of_sale/tests/test_pos_other_currency_config.py b/addons/point_of_sale/tests/test_pos_other_currency_config.py index 44a0552a0c9..86466df69bf 100644 --- a/addons/point_of_sale/tests/test_pos_other_currency_config.py +++ b/addons/point_of_sale/tests/test_pos_other_currency_config.py @@ -19,7 +19,14 @@ class TestPoSOtherCurrencyConfig(TestPoSCommon): self.product1 = self.create_product('Product 1', self.categ_basic, 10.0, 5) self.product2 = self.create_product('Product 2', self.categ_basic, 20.0, 10) self.product3 = self.create_product('Product 3', self.categ_basic, 30.0, 15) - self.adjust_inventory([self.product1, self.product2, self.product3], [100, 50, 50]) + self.product4 = self.create_product('Product 4', self.categ_anglo, 100, 50) + self.product5 = self.create_product('Product 5', self.categ_anglo, 200, 70) + self.product6 = self.create_product('Product 6', self.categ_anglo, 45.3, 10.73) + self.product7 = self.create_product('Product 7', self.categ_basic, 7, 7, tax_ids=self.taxes['tax7'].ids) + self.adjust_inventory( + [self.product1, self.product2, self.product3, self.product4, self.product5, self.product6, self.product7], + [100, 50, 50, 100, 100, 100, 100] + ) # change the price of product2 to 12.99 fixed. No need to convert. pricelist_item = self.env['product.pricelist.item'].create({ 'product_tmpl_id': self.product2.product_tmpl_id.id, @@ -27,6 +34,9 @@ class TestPoSOtherCurrencyConfig(TestPoSCommon): }) self.config.pricelist_id.write({'item_ids': [(6, 0, (self.config.pricelist_id.item_ids | pricelist_item).ids)]}) + self.output_account = self.categ_anglo.property_stock_account_output_categ_id + self.expense_account = self.categ_anglo.property_account_expense_categ_id + def test_01_check_product_cost(self): # Product price should be half of the original price because currency rate is 0.5. # (see `self._create_other_currency_config` method) @@ -35,6 +45,10 @@ class TestPoSOtherCurrencyConfig(TestPoSCommon): self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product1, 1, self.customer), 5.00) self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product2, 1, self.customer), 12.99) self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product3, 1, self.customer), 15.00) + self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product4, 1, self.customer), 50) + self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product5, 1, self.customer), 100) + self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product6, 1, self.customer), 22.65) + self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product7, 1, self.customer), 3.50) def test_02_orders_without_invoice(self): """ orders without invoice @@ -177,3 +191,110 @@ class TestPoSOtherCurrencyConfig(TestPoSCommon): invoice_receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.receivable_account) self.assertAlmostEqual(invoice_receivable_line.balance, -459.8) self.assertAlmostEqual(invoice_receivable_line.amount_currency, -229.9) + + def test_04_anglo_saxon_products(self): + """ + ====== + Orders + ====== + +---------+----------+-----------+----------+-----+----------+------------+ + | order | payments | invoiced? | product | qty | total | total cost | + | | | | | | | | + +---------+----------+-----------+----------+-----+----------+------------+ + | order 1 | cash | no | product4 | 7 | 700 | 350 | + | | | | product5 | 7 | 1400 | 490 | + +---------+----------+-----------+----------+-----+----------+------------+ + | order 2 | cash | no | product5 | 6 | 1200 | 420 | + | | | | product4 | 6 | 600 | 300 | + | | | | product6 | 49 | 2219.7 | 525.77 | + +---------+----------+-----------+----------+-----+----------+------------+ + | order 3 | cash | no | product5 | 2 | 400 | 140 | + | | | | product6 | 13 | 588.9 | 139.49 | + +---------+----------+-----------+----------+-----+----------+------------+ + | order 4 | cash | no | product6 | 1 | 45.3 | 10.73 | + +---------+----------+-----------+----------+-----+----------+------------+ + + =============== + Expected Result + =============== + +---------------------+------------+-----------------+ + | account | balance | amount_currency | + +---------------------+------------+-----------------+ + | sale_account | -7153.90 | -3576.95 | + | pos_receivable-cash | 7153.90 | 3576.95 | + | expense_account | 2375.99 | 0.00 | + | output_account | -2375.99 | 0.00 | + +---------------------+------------+-----------------+ + | Total balance | 0.00 | 0.00 | + +---------------------+------------+-----------------+ + """ + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product4, 7), (self.product5, 7)])) + orders.append(self.create_ui_order_data([(self.product5, 6), (self.product4, 6), (self.product6, 49)])) + orders.append(self.create_ui_order_data([(self.product5, 2), (self.product6, 13)])) + orders.append(self.create_ui_order_data([(self.product6, 1)])) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_account_move = self.pos_session.move_id + + self.assertEqual(len(session_account_move.line_ids), 4, msg='There should exactly be 4 account move lines.') + + sales_line = session_account_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sales_line.balance, -7153.90, msg='Sales line balance should be equal to total orders amount.') + self.assertAlmostEqual(sales_line.amount_currency, -3576.95) + + receivable_line_cash = session_account_move.line_ids.filtered(lambda line: self.pos_receivable_account == line.account_id and self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 7153.90, msg='Cash receivable should be equal to the total cash payments.') + self.assertAlmostEqual(receivable_line_cash.amount_currency, 3576.95) + + expense_line = session_account_move.line_ids.filtered(lambda line: line.account_id == self.expense_account) + self.assertAlmostEqual(expense_line.balance, 2375.99) + self.assertFalse(expense_line.currency_id, msg='Should be no currency in the stock expense line.') + self.assertAlmostEqual( + expense_line.amount_currency, 0.00, + msg="Should be zero because the balance is calculated from amounts in company's currency.", + ) + + output_line = session_account_move.line_ids.filtered(lambda line: line.account_id == self.output_account) + self.assertAlmostEqual(output_line.balance, -2375.99) + self.assertFalse(output_line.currency_id, msg='Should be no currency in the stock output line.') + self.assertAlmostEqual( + output_line.amount_currency, 0.00, + msg="Should be zero because the balance is calculated from amounts in company's currency.", + ) + + self.assertTrue(receivable_line_cash.full_reconcile_id, msg='Cash receivable line should be fully-reconciled.') + self.assertTrue(output_line.full_reconcile_id, msg='The stock output account line should be fully-reconciled.') + + def test_05_tax_base_amount(self): + self.open_new_session() + + order = self.env['pos.order'].create_from_ui( + [self.create_ui_order_data([(self.product7, 7)])] + ) + self.pos_session.action_pos_session_validate() + session_account_move = self.pos_session.move_id + + self.assertEqual(len(session_account_move.line_ids), 3, msg='There should exactly be 3 account move lines.') + + sales_line = session_account_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sales_line.balance, -49) + self.assertAlmostEqual(sales_line.amount_currency, -24.5) + + receivable_line_cash = session_account_move.line_ids.filtered(lambda line: self.pos_receivable_account == line.account_id and self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 52.44) + self.assertAlmostEqual(receivable_line_cash.amount_currency, 26.22) + + tax_line = session_account_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + self.assertAlmostEqual(tax_line.balance, -3.44) + self.assertAlmostEqual(tax_line.amount_currency, -1.72) + self.assertAlmostEqual(tax_line.tax_base_amount, 49, msg="Value should be in company's currency.")