diff options
| author | Joseph Caburnay <jcb@odoo.com> | 2019-12-05 10:23:23 +0000 |
|---|---|---|
| committer | Nicolas Martinelli <nim@odoo.com> | 2020-02-18 11:58:36 +0000 |
| commit | dc980e9448f10d6c31e81e4fec4911a95d8e6dd2 (patch) | |
| tree | 3c50701fd8b765c3aea34cda545946b32510fd49 | |
| parent | 38151f42c6d681a10984fe4d0efcd93c8ff00072 (diff) | |
[FIX] point_of_sale: anglo saxon and tax lines in different currency
The problem occurs when the pos session has currency that is different
from the company's currency. Additionally, product that is tracked in
accounting is also sold in this session.
This situation results to anglo saxon move lines that are incorrectly
converted.
The current implementation considers the value from the stock valuation
layer to be in the session's currency. However, that is not the real
case because values in the valuation layer are always in the company
currency.
With regards to tax line, the tax_base_amount is not converted to the
company's currency. It's value should always be in company's currency.
This commit fixes the behavior of pos in generating anglo saxon lines
and setting the correct tax_base_amount in the pos session's accounting
entry.
X-original-commit: 2bdbea3bbf87a921ed3b343f5146a519f10e0add
| -rw-r--r-- | addons/point_of_sale/models/pos_session.py | 126 | ||||
| -rw-r--r-- | addons/point_of_sale/tests/test_pos_other_currency_config.py | 123 |
2 files changed, 210 insertions, 39 deletions
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.") |
