diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/point_of_sale/tests')
16 files changed, 4794 insertions, 0 deletions
diff --git a/addons/point_of_sale/tests/__init__.py b/addons/point_of_sale/tests/__init__.py new file mode 100644 index 00000000..d108782c --- /dev/null +++ b/addons/point_of_sale/tests/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_point_of_sale_flow +from . import test_frontend +from . import test_point_of_sale_ui +from . import test_anglo_saxon +from . import test_point_of_sale +from . import test_pos_setup +from . import test_pos_basic_config +from . import test_pos_products_with_tax +from . import test_pos_multiple_sale_accounts +from . import test_pos_multiple_receivable_accounts +from . import test_pos_other_currency_config +from . import test_pos_with_fiscal_position +from . import test_pos_stock_account +from . import test_js diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py new file mode 100644 index 00000000..cf51f9fe --- /dev/null +++ b/addons/point_of_sale/tests/common.py @@ -0,0 +1,523 @@ +# -*- coding: utf-8 -*- +from random import randint + +from odoo import fields, tools +from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon +from odoo.tests.common import SavepointCase, Form +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestPointOfSaleCommon(ValuationReconciliationTestCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.company_data['company'].write({ + 'point_of_sale_update_stock_quantities': 'real', + }) + + cls.AccountBankStatement = cls.env['account.bank.statement'] + cls.AccountBankStatementLine = cls.env['account.bank.statement.line'] + cls.PosMakePayment = cls.env['pos.make.payment'] + cls.PosOrder = cls.env['pos.order'] + cls.PosSession = cls.env['pos.session'] + cls.company = cls.company_data['company'] + cls.product3 = cls.env['product.product'].create({ + 'name': 'Product 3', + 'list_price': 450, + }) + cls.product4 = cls.env['product.product'].create({ + 'name': 'Product 4', + 'list_price': 750, + }) + cls.partner1 = cls.env['res.partner'].create({'name': 'Partner 1'}) + cls.partner4 = cls.env['res.partner'].create({'name': 'Partner 4'}) + cls.pos_config = cls.env['pos.config'].create({ + 'name': 'Main', + 'journal_id': cls.company_data['default_journal_sale'].id, + 'invoice_journal_id': cls.company_data['default_journal_sale'].id, + }) + cls.led_lamp = cls.env['product.product'].create({ + 'name': 'LED Lamp', + 'available_in_pos': True, + 'list_price': 0.90, + }) + cls.whiteboard_pen = cls.env['product.product'].create({ + 'name': 'Whiteboard Pen', + 'available_in_pos': True, + 'list_price': 1.20, + }) + cls.newspaper_rack = cls.env['product.product'].create({ + 'name': 'Newspaper Rack', + 'available_in_pos': True, + 'list_price': 1.28, + }) + cls.cash_payment_method = cls.env['pos.payment.method'].create({ + 'name': 'Cash', + 'receivable_account_id': cls.company_data['default_account_receivable'].id, + 'is_cash_count': True, + 'cash_journal_id': cls.company_data['default_journal_cash'].id, + 'company_id': cls.env.company.id, + }) + cls.bank_payment_method = cls.env['pos.payment.method'].create({ + 'name': 'Bank', + 'receivable_account_id': cls.company_data['default_account_receivable'].id, + 'is_cash_count': False, + 'company_id': cls.env.company.id, + }) + cls.credit_payment_method = cls.env['pos.payment.method'].create({ + 'name': 'Credit', + 'receivable_account_id': cls.company_data['default_account_receivable'].id, + 'split_transactions': True, + 'company_id': cls.env.company.id, + }) + cls.pos_config.write({'payment_method_ids': [(4, cls.credit_payment_method.id), (4, cls.bank_payment_method.id), (4, cls.cash_payment_method.id)]}) + + # Create POS journal + cls.pos_config.journal_id = cls.env['account.journal'].create({ + 'type': 'sale', + 'name': 'Point of Sale - Test', + 'code': 'POSS - Test', + 'company_id': cls.env.company.id, + 'sequence': 20 + }) + + # create a VAT tax of 10%, included in the public price + Tax = cls.env['account.tax'] + account_tax_10_incl = Tax.create({ + 'name': 'VAT 10 perc Incl', + 'amount_type': 'percent', + 'amount': 10.0, + 'price_include': True, + }) + + # assign this 10 percent tax on the [PCSC234] PC Assemble SC234 product + # as a sale tax + cls.product3.taxes_id = [(6, 0, [account_tax_10_incl.id])] + + # create a VAT tax of 5%, which is added to the public price + account_tax_05_incl = Tax.create({ + 'name': 'VAT 5 perc Incl', + 'amount_type': 'percent', + 'amount': 5.0, + 'price_include': False, + }) + + # create a second VAT tax of 5% but this time for a child company, to + # ensure that only product taxes of the current session's company are considered + #(this tax should be ignore when computing order's taxes in following tests) + account_tax_05_incl_chicago = Tax.create({ + 'name': 'VAT 05 perc Excl (US)', + 'amount_type': 'percent', + 'amount': 5.0, + 'price_include': False, + 'company_id': cls.company_data_2['company'].id, + }) + + cls.product4.company_id = False + # I assign those 5 percent taxes on the PCSC349 product as a sale taxes + cls.product4.write( + {'taxes_id': [(6, 0, [account_tax_05_incl.id, account_tax_05_incl_chicago.id])]}) + + # Set account_id in the generated repartition lines. Automatically, nothing is set. + invoice_rep_lines = (account_tax_05_incl | account_tax_10_incl).mapped('invoice_repartition_line_ids') + refund_rep_lines = (account_tax_05_incl | account_tax_10_incl).mapped('refund_repartition_line_ids') + + # Expense account, should just be something else than receivable/payable + (invoice_rep_lines | refund_rep_lines).write({'account_id': cls.company_data['default_account_tax_sale'].id}) + + +@tagged('post_install', '-at_install') +class TestPoSCommon(ValuationReconciliationTestCommon): + """ Set common values for different special test cases. + + The idea is to set up common values here for the tests + and implement different special scenarios by inheriting + this class. + """ + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.company_data['company'].write({ + 'point_of_sale_update_stock_quantities': 'real', + }) + + # Set basic defaults + cls.company = cls.company_data['company'] + cls.pos_sale_journal = cls.env['account.journal'].create({ + 'type': 'sale', + 'name': 'Point of Sale Test', + 'code': 'POST', + 'company_id': cls.company.id, + 'sequence': 20 + }) + cls.invoice_journal = cls.company_data['default_journal_sale'] + cls.receivable_account = cls.company_data['default_account_receivable'] + cls.tax_received_account = cls.company_data['default_account_tax_sale'] + cls.company.account_default_pos_receivable_account_id = cls.env['account.account'].create({ + 'code': 'X1012 - POS', + 'name': 'Debtors - (POS)', + 'reconcile': True, + 'user_type_id': cls.env.ref('account.data_account_type_receivable').id, + }) + cls.pos_receivable_account = cls.company.account_default_pos_receivable_account_id + cls.other_receivable_account = cls.env['account.account'].create({ + 'name': 'Other Receivable', + 'code': 'RCV00' , + 'user_type_id': cls.env['account.account.type'].create({'name': 'RCV type', 'type': 'receivable', 'internal_group': 'asset'}).id, + 'internal_group': 'asset', + 'reconcile': True, + }) + + # company_currency can be different from `base.USD` depending on the localization installed + cls.company_currency = cls.company.currency_id + # other_currency is a currency different from the company_currency + # sometimes company_currency is different from USD, so handle appropriately. + cls.other_currency = cls.currency_data['currency'] + + cls.currency_pricelist = cls.env['product.pricelist'].create({ + 'name': 'Public Pricelist', + 'currency_id': cls.company_currency.id, + }) + # Set Point of Sale configurations + # basic_config + # - derived from 'point_of_sale.pos_config_main' with added invoice_journal_id and credit payment method. + # other_currency_config + # - pos.config set to have currency different from company currency. + cls.basic_config = cls._create_basic_config() + cls.other_currency_config = cls._create_other_currency_config() + + # Set product categories + # categ_basic + # - just the plain 'product.product_category_all' + # categ_anglo + # - product category with fifo and real_time valuations + # - used for checking anglo saxon accounting behavior + cls.categ_basic = cls.env.ref('product.product_category_all') + cls.env.company.anglo_saxon_accounting = True + cls.categ_anglo = cls._create_categ_anglo() + + # other basics + cls.sale_account = cls.categ_basic.property_account_income_categ_id + cls.other_sale_account = cls.env['account.account'].search([ + ('company_id', '=', cls.company.id), + ('user_type_id', '=', cls.env.ref('account.data_account_type_revenue').id), + ('id', '!=', cls.sale_account.id) + ], limit=1) + + # Set customers + cls.customer = cls.env['res.partner'].create({'name': 'Test Customer'}) + cls.other_customer = cls.env['res.partner'].create({'name': 'Other Customer', 'property_account_receivable_id': cls.other_receivable_account.id}) + + # Set taxes + # cls.taxes => dict + # keys: 'tax7', 'tax10'(price_include=True), 'tax_group_7_10' + cls.taxes = cls._create_taxes() + + cls.stock_location_components = cls.env["stock.location"].create({ + 'name': 'Shelf 1', + 'location_id': cls.company_data['default_warehouse'].lot_stock_id.id, + }) + + ##################### + ## private methods ## + ##################### + + @classmethod + def _create_basic_config(cls): + new_config = Form(cls.env['pos.config']) + new_config.name = 'PoS Shop Test' + new_config.module_account = True + new_config.invoice_journal_id = cls.invoice_journal + new_config.journal_id = cls.pos_sale_journal + new_config.available_pricelist_ids.clear() + new_config.available_pricelist_ids.add(cls.currency_pricelist) + new_config.pricelist_id = cls.currency_pricelist + config = new_config.save() + cash_payment_method = cls.env['pos.payment.method'].create({ + 'name': 'Cash', + 'receivable_account_id': cls.pos_receivable_account.id, + 'is_cash_count': True, + 'cash_journal_id': cls.company_data['default_journal_cash'].id, + 'company_id': cls.env.company.id, + }) + bank_payment_method = cls.env['pos.payment.method'].create({ + 'name': 'Bank', + 'receivable_account_id': cls.pos_receivable_account.id, + 'is_cash_count': False, + 'company_id': cls.env.company.id, + }) + cash_split_pm = cls.env['pos.payment.method'].create({ + 'name': 'Split (Cash) PM', + 'receivable_account_id': cls.pos_receivable_account.id, + 'split_transactions': True, + 'is_cash_count': True, + 'cash_journal_id': cls.company_data['default_journal_cash'].id, + }) + bank_split_pm = cls.env['pos.payment.method'].create({ + 'name': 'Split (Bank) PM', + 'receivable_account_id': cls.pos_receivable_account.id, + 'split_transactions': True, + }) + config.write({'payment_method_ids': [(4, cash_split_pm.id), (4, bank_split_pm.id), (4, cash_payment_method.id), (4, bank_payment_method.id)]}) + return config + + @classmethod + def _create_other_currency_config(cls): + (cls.other_currency.rate_ids | cls.company_currency.rate_ids).unlink() + cls.env['res.currency.rate'].create({ + 'rate': 0.5, + 'currency_id': cls.other_currency.id, + }) + other_cash_journal = cls.env['account.journal'].create({ + 'name': 'Cash Other', + 'type': 'cash', + 'company_id': cls.company.id, + 'code': 'CSHO', + 'sequence': 10, + 'currency_id': cls.other_currency.id + }) + other_invoice_journal = cls.env['account.journal'].create({ + 'name': 'Customer Invoice Other', + 'type': 'sale', + 'company_id': cls.company.id, + 'code': 'INVO', + 'sequence': 11, + 'currency_id': cls.other_currency.id + }) + other_sales_journal = cls.env['account.journal'].create({ + 'name':'PoS Sale Other', + 'type': 'sale', + 'code': 'POSO', + 'company_id': cls.company.id, + 'sequence': 12, + 'currency_id': cls.other_currency.id + }) + other_pricelist = cls.env['product.pricelist'].create({ + 'name': 'Public Pricelist Other', + 'currency_id': cls.other_currency.id, + }) + other_cash_payment_method = cls.env['pos.payment.method'].create({ + 'name': 'Cash Other', + 'receivable_account_id': cls.pos_receivable_account.id, + 'is_cash_count': True, + 'cash_journal_id': other_cash_journal.id, + }) + other_bank_payment_method = cls.env['pos.payment.method'].create({ + 'name': 'Bank Other', + 'receivable_account_id': cls.pos_receivable_account.id, + }) + + new_config = Form(cls.env['pos.config']) + new_config.name = 'Shop Other' + new_config.invoice_journal_id = other_invoice_journal + new_config.journal_id = other_sales_journal + new_config.use_pricelist = True + new_config.available_pricelist_ids.clear() + new_config.available_pricelist_ids.add(other_pricelist) + new_config.pricelist_id = other_pricelist + new_config.payment_method_ids.clear() + new_config.payment_method_ids.add(other_cash_payment_method) + new_config.payment_method_ids.add(other_bank_payment_method) + config = new_config.save() + return config + + @classmethod + def _create_categ_anglo(cls): + return cls.env['product.category'].create({ + 'name': 'Anglo', + 'parent_id': False, + 'property_cost_method': 'fifo', + 'property_valuation': 'real_time', + 'property_stock_account_input_categ_id': cls.company_data['default_account_stock_in'].id, + 'property_stock_account_output_categ_id': cls.company_data['default_account_stock_out'].id, + }) + + @classmethod + def _create_taxes(cls): + """ Create taxes + + tax7: 7%, excluded in product price + tax10: 10%, included in product price + """ + tax7 = cls.env['account.tax'].create({'name': 'Tax 7%', 'amount': 7}) + tax10 = cls.env['account.tax'].create({'name': 'Tax 10%', 'amount': 10, 'price_include': True, 'include_base_amount': False}) + (tax7 | tax10).mapped('invoice_repartition_line_ids').write({'account_id': cls.tax_received_account.id}) + (tax7 | tax10).mapped('refund_repartition_line_ids').write({'account_id': cls.tax_received_account.id}) + + tax_group_7_10 = tax7.copy() + with Form(tax_group_7_10) as tax: + tax.name = 'Tax 7+10%' + tax.amount_type = 'group' + tax.children_tax_ids.add(tax7) + tax.children_tax_ids.add(tax10) + + return { + 'tax7': tax7, + 'tax10': tax10, + 'tax_group_7_10': tax_group_7_10 + } + + #################### + ## public methods ## + #################### + + def create_random_uid(self): + return ('%05d-%03d-%04d' % (randint(1, 99999), randint(1, 999), randint(1, 9999))) + + def create_ui_order_data(self, product_quantity_pairs, customer=False, is_invoiced=False, payments=None, uid=None): + """ Mocks the order_data generated by the pos ui. + + This is useful in making orders in an open pos session without making tours. + Its functionality is tested in test_pos_create_ui_order_data.py. + + Before use, make sure that self is set with: + 1. pricelist -> the pricelist of the current session + 2. currency -> currency of the current session + 3. pos_session -> the current session, equivalent to config.current_session_id + 4. cash_pm -> first cash payment method in the current session + 5. config -> the active pos.config + + The above values should be set when `self.open_new_session` is called. + + :param list(tuple) product_quantity_pairs: pair of `ordered product` and `quantity` + :param list(tuple) payments: pair of `payment_method` and `amount` + """ + default_fiscal_position = self.config.default_fiscal_position_id + fiscal_position = customer.property_account_position_id if customer else default_fiscal_position + + def create_order_line(product, quantity): + price_unit = self.pricelist.get_product_price(product, quantity, False) + tax_ids = fiscal_position.map_tax(product.taxes_id) + tax_values = ( + tax_ids.compute_all(price_unit, self.currency, quantity) + if tax_ids + else { + 'total_excluded': price_unit * quantity, + 'total_included': price_unit * quantity, + } + ) + return (0, 0, { + 'discount': 0, + 'id': randint(1, 1000000), + 'pack_lot_ids': [], + 'price_unit': price_unit, + 'product_id': product.id, + 'price_subtotal': tax_values['total_excluded'], + 'price_subtotal_incl': tax_values['total_included'], + 'qty': quantity, + 'tax_ids': [(6, 0, tax_ids.ids)] + }) + + def create_payment(payment_method, amount): + return (0, 0, { + 'amount': amount, + 'name': fields.Datetime.now(), + 'payment_method_id': payment_method.id, + }) + + uid = uid or self.create_random_uid() + + # 1. generate the order lines + order_lines = [create_order_line(product, quantity) for product, quantity in product_quantity_pairs] + + # 2. generate the payments + total_amount_incl = sum(line[2]['price_subtotal_incl'] for line in order_lines) + if payments is None: + payments = [create_payment(self.cash_pm, total_amount_incl)] + else: + payments = [ + create_payment(pm, amount) + for pm, amount in payments + ] + + # 3. complete the fields of the order_data + total_amount_base = sum(line[2]['price_subtotal'] for line in order_lines) + return { + 'data': { + 'amount_paid': sum(payment[2]['amount'] for payment in payments), + 'amount_return': 0, + 'amount_tax': total_amount_incl - total_amount_base, + 'amount_total': total_amount_incl, + 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), + 'fiscal_position_id': fiscal_position.id, + 'pricelist_id': self.config.pricelist_id.id, + 'lines': order_lines, + 'name': 'Order %s' % uid, + 'partner_id': customer and customer.id, + 'pos_session_id': self.pos_session.id, + 'sequence_number': 2, + 'statement_ids': payments, + 'uid': uid, + 'user_id': self.env.user.id, + 'to_invoice': is_invoiced, + }, + 'id': uid, + 'to_invoice': is_invoiced, + } + + @classmethod + def create_product(cls, name, category, lst_price, standard_price=None, tax_ids=None, sale_account=None): + product = cls.env['product.product'].create({ + 'type': 'product', + 'available_in_pos': True, + 'taxes_id': [(5, 0, 0)] if not tax_ids else [(6, 0, tax_ids)], + 'name': name, + 'categ_id': category.id, + 'lst_price': lst_price, + 'standard_price': standard_price if standard_price else 0.0, + }) + if sale_account: + product.property_account_income_id = sale_account + return product + + @classmethod + def adjust_inventory(cls, products, quantities): + """ Adjust inventory of the given products + """ + inventory = cls.env['stock.inventory'].create({ + 'name': 'Inventory adjustment' + }) + for product, qty in zip(products, quantities): + cls.env['stock.inventory.line'].create({ + 'product_id': product.id, + 'product_uom_id': cls.env.ref('uom.product_uom_unit').id, + 'inventory_id': inventory.id, + 'product_qty': qty, + 'location_id': cls.stock_location_components.id, + }) + inventory._action_start() + inventory.action_validate() + + def open_new_session(self): + """ Used to open new pos session in each configuration. + + - The idea is to properly set values that are constant + and commonly used in an open pos session. + - Calling this method is also a prerequisite for using + `self.create_ui_order_data` function. + + Fields: + * config : the pos.config currently being used. + Its value is set at `self.setUp` of the inheriting + test class. + * session : the current_session_id of config + * currency : currency of the current pos.session + * pricelist : the default pricelist of the session + * cash_pm : cash payment method of the session + * bank_pm : bank payment method of the session + * cash_split_pm : credit payment method of the session + * bank_split_pm : split bank payment method of the session + """ + self.config.open_session_cb(check_coa=False) + self.pos_session = self.config.current_session_id + self.currency = self.pos_session.currency_id + self.pricelist = self.pos_session.config_id.pricelist_id + self.cash_pm = self.pos_session.payment_method_ids.filtered(lambda pm: pm.is_cash_count and not pm.split_transactions)[:1] + self.bank_pm = self.pos_session.payment_method_ids.filtered(lambda pm: not pm.is_cash_count and not pm.split_transactions)[:1] + self.cash_split_pm = self.pos_session.payment_method_ids.filtered(lambda pm: pm.is_cash_count and pm.split_transactions)[:1] + self.bank_split_pm = self.pos_session.payment_method_ids.filtered(lambda pm: not pm.is_cash_count and pm.split_transactions)[:1] diff --git a/addons/point_of_sale/tests/test_anglo_saxon.py b/addons/point_of_sale/tests/test_anglo_saxon.py new file mode 100644 index 00000000..aed16559 --- /dev/null +++ b/addons/point_of_sale/tests/test_anglo_saxon.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo +import time +from odoo import fields +from odoo.tests import common + +class TestAngloSaxonCommon(common.TransactionCase): + + def setUp(self): + super(TestAngloSaxonCommon, self).setUp() + self.PosMakePayment = self.env['pos.make.payment'] + self.PosOrder = self.env['pos.order'] + self.Statement = self.env['account.bank.statement'] + self.company = self.env.ref('base.main_company') + self.warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1) + self.partner = self.env['res.partner'].create({'name': 'Partner 1'}) + self.category = self.env.ref('product.product_category_all') + self.category = self.category.copy({'name': 'New category','property_valuation': 'real_time'}) + account_type_rcv = self.env.ref('account.data_account_type_receivable') + account_type_inc = self.env.ref('account.data_account_type_revenue') + account_type_exp = self.env.ref('account.data_account_type_expenses') + self.account = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00' , 'user_type_id': account_type_rcv.id, 'reconcile': True}) + account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00' , 'user_type_id': account_type_exp.id, 'reconcile': True}) + account_income = self.env['account.account'].create({'name': 'Income', 'code': 'INC00' , 'user_type_id': account_type_inc.id, 'reconcile': True}) + account_output = self.env['account.account'].create({'name': 'Output', 'code': 'OUT00' , 'user_type_id': account_type_exp.id, 'reconcile': True}) + account_valuation = self.env['account.account'].create({'name': 'Valuation', 'code': 'STV00', 'user_type_id': account_type_exp.id, 'reconcile': True}) + self.partner.property_account_receivable_id = self.account + self.category.property_account_income_categ_id = account_income + self.category.property_account_expense_categ_id = account_expense + self.category.property_stock_account_input_categ_id = self.account + self.category.property_stock_account_output_categ_id = account_output + self.category.property_stock_valuation_account_id = account_valuation + self.category.property_stock_journal = self.env['account.journal'].create({'name': 'Stock journal', 'type': 'sale', 'code': 'STK00'}) + self.pos_config = self.env.ref('point_of_sale.pos_config_main') + self.pos_config = self.pos_config.copy({'name': 'New POS config'}) + self.product = self.env['product.product'].create({ + 'name': 'New product', + 'standard_price': 100, + 'available_in_pos': True, + 'type': 'product', + }) + self.company.anglo_saxon_accounting = True + self.company.point_of_sale_update_stock_quantities = 'real' + self.product.categ_id = self.category + self.product.property_account_expense_id = account_expense + self.product.property_account_income_id = account_income + sale_journal = self.env['account.journal'].create({'name': 'POS journal', 'type': 'sale', 'code': 'POS00'}) + self.pos_config.journal_id = sale_journal + self.cash_journal = self.env['account.journal'].create({'name': 'CASH journal', 'type': 'cash', 'code': 'CSH00'}) + self.sale_journal = self.env['account.journal'].create({'name': 'SALE journal', 'type': 'sale', 'code': 'INV00'}) + self.pos_config.invoice_journal_id = self.sale_journal + self.cash_payment_method = self.env['pos.payment.method'].create({ + 'name': 'Cash Test', + 'is_cash_count': True, + 'cash_journal_id': self.cash_journal.id, + 'receivable_account_id': self.account.id, + }) + self.pos_config.write({'payment_method_ids': [(6, 0, self.cash_payment_method.ids)]}) + + +@odoo.tests.tagged('post_install', '-at_install') +class TestAngloSaxonFlow(TestAngloSaxonCommon): + + def test_create_account_move_line(self): + # This test will check that the correct journal entries are created when a product in real time valuation + # is sold in a company using anglo-saxon + self.pos_config.open_session_cb(check_coa=False) + current_session = self.pos_config.current_session_id + self.cash_journal.loss_account_id = self.account + + # I create a PoS order with 1 unit of New product at 450 EUR + self.pos_order_pos0 = self.PosOrder.create({ + 'company_id': self.company.id, + 'partner_id': self.partner.id, + 'pricelist_id': self.company.partner_id.property_product_pricelist.id, + 'session_id': self.pos_config.current_session_id.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': 1.0, + 'price_subtotal': 450, + 'price_subtotal_incl': 450, + })], + 'amount_total': 450, + 'amount_tax': 0, + 'amount_paid': 0, + 'amount_return': 0, + }) + + # I make a payment to fully pay the order + context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} + self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 450.0, + 'payment_method_id': self.cash_payment_method.id, + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos0.id} + self.pos_make_payment_0.with_context(context_payment).check() + + # I check that the order is marked as paid + self.assertEqual(self.pos_order_pos0.state, 'paid', 'Order should be in paid state.') + self.assertEqual(self.pos_order_pos0.amount_paid, 450, 'Amount paid for the order should be updated.') + + # I close the current session to generate the journal entries + current_session_id = self.pos_config.current_session_id + current_session_id._check_pos_session_balance() + current_session_id.action_pos_session_close() + self.assertEqual(current_session_id.state, 'closed', 'Check that session is closed') + + # Check if there is account_move in the order. + # There shouldn't be because the order is not invoiced. + self.assertFalse(self.pos_order_pos0.account_move, 'There should be no invoice in the order.') + + # I test that the generated journal entries are correct. + account_output = self.category.property_stock_account_output_categ_id + expense_account = self.category.property_account_expense_categ_id + aml = current_session.move_id.line_ids + aml_output = aml.filtered(lambda l: l.account_id.id == account_output.id) + aml_expense = aml.filtered(lambda l: l.account_id.id == expense_account.id) + self.assertEqual(aml_output.credit, self.product.standard_price, "Cost of Good Sold entry missing or mismatching") + self.assertEqual(aml_expense.debit, self.product.standard_price, "Cost of Good Sold entry missing or mismatching") + + def _prepare_pos_order(self): + """ Set the cost method of `self.product` as FIFO. Receive 5@5 and 5@1 and + create a `pos.order` record selling 7 units @ 450. + """ + # check fifo Costing Method of product.category + self.product.categ_id.property_cost_method = 'fifo' + self.product.standard_price = 5.0 + self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': self.product.id, + 'inventory_quantity': 5.0, + 'location_id': self.warehouse.lot_stock_id.id, + }) + self.product.standard_price = 1.0 + self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': self.product.id, + 'inventory_quantity': 10.0, + 'location_id': self.warehouse.lot_stock_id.id, + }) + self.assertEqual(self.product.value_svl, 30, "Value should be (5*5 + 5*1) = 30") + self.assertEqual(self.product.quantity_svl, 10) + + self.pos_config.module_account = True + self.pos_config.open_session_cb(check_coa=False) + + pos_order_values = { + 'company_id': self.company.id, + 'partner_id': self.partner.id, + 'pricelist_id': self.company.partner_id.property_product_pricelist.id, + 'session_id': self.pos_config.current_session_id.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': 7.0, + 'price_subtotal': 7 * 450, + 'price_subtotal_incl': 7 * 450, + })], + 'amount_total': 7 * 450, + 'amount_tax': 0, + 'amount_paid': 0, + 'amount_return': 0, + } + + return self.PosOrder.create(pos_order_values) + + def test_fifo_valuation_no_invoice(self): + """Register a payment and validate a session after selling a fifo + product without making an invoice for the customer""" + pos_order_pos0 = self._prepare_pos_order() + context_make_payment = {"active_ids": [pos_order_pos0.id], "active_id": pos_order_pos0.id} + self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 7 * 450.0, + 'payment_method_id': self.cash_payment_method.id, + }) + + # register the payment + context_payment = {'active_id': pos_order_pos0.id} + self.pos_make_payment_0.with_context(context_payment).check() + + # validate the session + current_session_id = self.pos_config.current_session_id + current_session_id.action_pos_session_validate() + + # check the anglo saxon move lines + # with uninvoiced orders, the account_move field of pos.order is empty. + # the accounting lines are in move_id of pos.session. + session_move = pos_order_pos0.session_id.move_id + line = session_move.line_ids.filtered(lambda l: l.debit and l.account_id == self.category.property_account_expense_categ_id) + self.assertEqual(session_move.journal_id, self.pos_config.journal_id) + self.assertEqual(line.debit, 27, 'As it is a fifo product, the move\'s value should be 5*5 + 2*1') + + def test_fifo_valuation_with_invoice(self): + """Register a payment and validate a session after selling a fifo + product and make an invoice for the customer""" + pos_order_pos0 = self._prepare_pos_order() + context_make_payment = {"active_ids": [pos_order_pos0.id], "active_id": pos_order_pos0.id} + self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 7 * 450.0, + 'payment_method_id': self.cash_payment_method.id, + }) + + # register the payment + context_payment = {'active_id': pos_order_pos0.id} + self.pos_make_payment_0.with_context(context_payment).check() + + # Create the customer invoice + pos_order_pos0.action_pos_order_invoice() + + # validate the session + current_session_id = self.pos_config.current_session_id + current_session_id.action_pos_session_validate() + + # check the anglo saxon move lines + line = pos_order_pos0.account_move.line_ids.filtered(lambda l: l.debit and l.account_id == self.category.property_account_expense_categ_id) + self.assertEqual(pos_order_pos0.account_move.journal_id, self.pos_config.invoice_journal_id) + self.assertEqual(line.debit, 27, 'As it is a fifo product, the move\'s value should be 5*5 + 2*1') diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py new file mode 100644 index 00000000..40e31e1c --- /dev/null +++ b/addons/point_of_sale/tests/test_frontend.py @@ -0,0 +1,525 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import tools +from odoo.api import Environment +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from datetime import date, timedelta + +import odoo.tests + + +class TestPointOfSaleHttpCommon(odoo.tests.HttpCase): + + def setUp(self): + super().setUp() + env = self.env(user=self.env.ref('base.user_admin')) + + journal_obj = env['account.journal'] + account_obj = env['account.account'] + main_company = env.ref('base.main_company') + self.main_pos_config = env.ref('point_of_sale.pos_config_main') + + env['res.partner'].create({ + 'name': 'Deco Addict', + }) + + account_receivable = account_obj.create({'code': 'X1012', + 'name': 'Account Receivable - Test', + 'user_type_id': env.ref('account.data_account_type_receivable').id, + 'reconcile': True}) + self.env.company.account_default_pos_receivable_account_id = account_receivable + + self.env['ir.property']._set_default('property_account_receivable_id', 'res.partner', account_receivable, main_company) + + cash_journal = journal_obj.create({ + 'name': 'Cash Test', + 'type': 'cash', + 'company_id': main_company.id, + 'code': 'CSH', + 'sequence': 10, + }) + + # Archive all existing product to avoid noise during the tours + all_pos_product = self.env['product.product'].search([('available_in_pos', '=', True)]) + discount = self.env.ref('point_of_sale.product_product_consumable') + self.tip = self.env.ref('point_of_sale.product_product_tip') + (all_pos_product - discount - self.tip)._write({'active': False}) + + # In DESKS categ: Desk Pad + pos_categ_desks = env.ref('point_of_sale.pos_category_desks') + + # In DESKS categ: Whiteboard Pen + pos_categ_misc = env.ref('point_of_sale.pos_category_miscellaneous') + + # In CHAIR categ: Letter Tray + pos_categ_chairs = env.ref('point_of_sale.pos_category_chairs') + + # test an extra price on an attribute + pear = env['product.product'].create({ + 'name': 'Whiteboard Pen', + 'available_in_pos': True, + 'list_price': 1.20, + 'taxes_id': False, + 'weight': 0.01, + 'to_weight': True, + 'pos_categ_id': pos_categ_misc.id, + }) + wall_shelf = env['product.product'].create({ + 'name': 'Wall Shelf Unit', + 'available_in_pos': True, + 'list_price': 1.98, + 'taxes_id': False, + }) + small_shelf = env['product.product'].create({ + 'name': 'Small Shelf', + 'available_in_pos': True, + 'list_price': 2.83, + 'taxes_id': False, + }) + magnetic_board = env['product.product'].create({ + 'name': 'Magnetic Board', + 'available_in_pos': True, + 'list_price': 1.98, + 'taxes_id': False, + }) + monitor_stand = env['product.product'].create({ + 'name': 'Monitor Stand', + 'available_in_pos': True, + 'list_price': 3.19, + 'taxes_id': False, + }) + desk_pad = env['product.product'].create({ + 'name': 'Desk Pad', + 'available_in_pos': True, + 'list_price': 1.98, + 'taxes_id': False, + 'pos_categ_id': pos_categ_desks.id, + }) + letter_tray = env['product.product'].create({ + 'name': 'Letter Tray', + 'available_in_pos': True, + 'list_price': 4.80, + 'taxes_id': False, + 'pos_categ_id': pos_categ_chairs.id, + }) + desk_organizer = env['product.product'].create({ + 'name': 'Desk Organizer', + 'available_in_pos': True, + 'list_price': 5.10, + 'taxes_id': False, + }) + configurable_chair = env['product.product'].create({ + 'name': 'Configurable Chair', + 'available_in_pos': True, + 'list_price': 10, + 'taxes_id': False, + }) + + attribute = env['product.attribute'].create({ + 'name': 'add 2', + }) + attribute_value = env['product.attribute.value'].create({ + 'name': 'add 2', + 'attribute_id': attribute.id, + }) + line = env['product.template.attribute.line'].create({ + 'product_tmpl_id': pear.product_tmpl_id.id, + 'attribute_id': attribute.id, + 'value_ids': [(6, 0, attribute_value.ids)] + }) + line.product_template_value_ids[0].price_extra = 2 + + chair_color_attribute = env['product.attribute'].create({ + 'name': 'Color', + 'display_type': 'color', + 'create_variant': 'no_variant', + }) + chair_color_red = env['product.attribute.value'].create({ + 'name': 'Red', + 'attribute_id': chair_color_attribute.id, + 'html_color': '#ff0000', + }) + chair_color_blue = env['product.attribute.value'].create({ + 'name': 'Blue', + 'attribute_id': chair_color_attribute.id, + 'html_color': '#0000ff', + }) + chair_color_line = env['product.template.attribute.line'].create({ + 'product_tmpl_id': configurable_chair.product_tmpl_id.id, + 'attribute_id': chair_color_attribute.id, + 'value_ids': [(6, 0, [chair_color_red.id, chair_color_blue.id])] + }) + chair_color_line.product_template_value_ids[0].price_extra = 1 + + chair_legs_attribute = env['product.attribute'].create({ + 'name': 'Chair Legs', + 'display_type': 'select', + 'create_variant': 'no_variant', + }) + chair_legs_metal = env['product.attribute.value'].create({ + 'name': 'Metal', + 'attribute_id': chair_legs_attribute.id, + }) + chair_legs_wood = env['product.attribute.value'].create({ + 'name': 'Wood', + 'attribute_id': chair_legs_attribute.id, + }) + chair_legs_line = env['product.template.attribute.line'].create({ + 'product_tmpl_id': configurable_chair.product_tmpl_id.id, + 'attribute_id': chair_legs_attribute.id, + 'value_ids': [(6, 0, [chair_legs_metal.id, chair_legs_wood.id])] + }) + + chair_fabrics_attribute = env['product.attribute'].create({ + 'name': 'Fabrics', + 'display_type': 'radio', + 'create_variant': 'no_variant', + }) + chair_fabrics_leather = env['product.attribute.value'].create({ + 'name': 'Leather', + 'attribute_id': chair_fabrics_attribute.id, + }) + chair_fabrics_other = env['product.attribute.value'].create({ + 'name': 'Other', + 'attribute_id': chair_fabrics_attribute.id, + 'is_custom': True, + }) + chair_fabrics_line = env['product.template.attribute.line'].create({ + 'product_tmpl_id': configurable_chair.product_tmpl_id.id, + 'attribute_id': chair_fabrics_attribute.id, + 'value_ids': [(6, 0, [chair_fabrics_leather.id, chair_fabrics_other.id])] + }) + chair_color_line.product_template_value_ids[1].is_custom = True + + fixed_pricelist = env['product.pricelist'].create({ + 'name': 'Fixed', + 'item_ids': [(0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 1, + }), (0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 2, + 'applied_on': '0_product_variant', + 'product_id': wall_shelf.id, + }), (0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 13.95, # test for issues like in 7f260ab517ebde634fc274e928eb062463f0d88f + 'applied_on': '0_product_variant', + 'product_id': small_shelf.id, + })], + }) + + env['product.pricelist'].create({ + 'name': 'Percentage', + 'item_ids': [(0, 0, { + 'compute_price': 'percentage', + 'percent_price': 100, + 'applied_on': '0_product_variant', + 'product_id': wall_shelf.id, + }), (0, 0, { + 'compute_price': 'percentage', + 'percent_price': 99, + 'applied_on': '0_product_variant', + 'product_id': small_shelf.id, + }), (0, 0, { + 'compute_price': 'percentage', + 'percent_price': 0, + 'applied_on': '0_product_variant', + 'product_id': magnetic_board.id, + })], + }) + + env['product.pricelist'].create({ + 'name': 'Formula', + 'item_ids': [(0, 0, { + 'compute_price': 'formula', + 'price_discount': 6, + 'price_surcharge': 5, + 'applied_on': '0_product_variant', + 'product_id': wall_shelf.id, + }), (0, 0, { + # .99 prices + 'compute_price': 'formula', + 'price_surcharge': -0.01, + 'price_round': 1, + 'applied_on': '0_product_variant', + 'product_id': small_shelf.id, + }), (0, 0, { + 'compute_price': 'formula', + 'price_min_margin': 10, + 'price_max_margin': 100, + 'applied_on': '0_product_variant', + 'product_id': magnetic_board.id, + }), (0, 0, { + 'compute_price': 'formula', + 'price_surcharge': 10, + 'price_max_margin': 5, + 'applied_on': '0_product_variant', + 'product_id': monitor_stand.id, + }), (0, 0, { + 'compute_price': 'formula', + 'price_discount': -100, + 'price_min_margin': 5, + 'price_max_margin': 20, + 'applied_on': '0_product_variant', + 'product_id': desk_pad.id, + })], + }) + + env['product.pricelist'].create({ + 'name': 'min_quantity ordering', + 'item_ids': [(0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 1, + 'applied_on': '0_product_variant', + 'min_quantity': 2, + 'product_id': wall_shelf.id, + }), (0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 2, + 'applied_on': '0_product_variant', + 'min_quantity': 1, + 'product_id': wall_shelf.id, + }), (0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 2, + 'applied_on': '0_product_variant', + 'min_quantity': 2, + 'product_id': env.ref('point_of_sale.product_product_consumable').id, + })], + }) + + env['product.pricelist'].create({ + 'name': 'Product template', + 'item_ids': [(0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 1, + 'applied_on': '1_product', + 'product_tmpl_id': wall_shelf.product_tmpl_id.id, + }), (0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 2, + })], + }) + + product_category_3 = env['product.category'].create({ + 'name': 'Services', + 'parent_id': env.ref('product.product_category_1').id, + }) + + env['product.pricelist'].create({ + # no category has precedence over category + 'name': 'Category vs no category', + 'item_ids': [(0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 1, + 'applied_on': '2_product_category', + 'categ_id': product_category_3.id, # All / Saleable / Services + }), (0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 2, + })], + }) + + p = env['product.pricelist'].create({ + 'name': 'Category', + 'item_ids': [(0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 2, + 'applied_on': '2_product_category', + 'categ_id': env.ref('product.product_category_all').id, + }), (0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 1, + 'applied_on': '2_product_category', + 'categ_id': product_category_3.id, # All / Saleable / Services + })], + }) + + today = date.today() + one_week_ago = today - timedelta(weeks=1) + two_weeks_ago = today - timedelta(weeks=2) + one_week_from_now = today + timedelta(weeks=1) + two_weeks_from_now = today + timedelta(weeks=2) + + public_pricelist = env['product.pricelist'].create({ + 'name': 'Public Pricelist', + }) + + env['product.pricelist'].create({ + 'name': 'Dates', + 'item_ids': [(0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 1, + 'date_start': two_weeks_ago.strftime(DEFAULT_SERVER_DATE_FORMAT), + 'date_end': one_week_ago.strftime(DEFAULT_SERVER_DATE_FORMAT), + }), (0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 2, + 'date_start': today.strftime(DEFAULT_SERVER_DATE_FORMAT), + 'date_end': one_week_from_now.strftime(DEFAULT_SERVER_DATE_FORMAT), + }), (0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 3, + 'date_start': one_week_from_now.strftime(DEFAULT_SERVER_DATE_FORMAT), + 'date_end': two_weeks_from_now.strftime(DEFAULT_SERVER_DATE_FORMAT), + })], + }) + + cost_base_pricelist = env['product.pricelist'].create({ + 'name': 'Cost base', + 'item_ids': [(0, 0, { + 'base': 'standard_price', + 'compute_price': 'percentage', + 'percent_price': 55, + })], + }) + + pricelist_base_pricelist = env['product.pricelist'].create({ + 'name': 'Pricelist base', + 'item_ids': [(0, 0, { + 'base': 'pricelist', + 'base_pricelist_id': cost_base_pricelist.id, + 'compute_price': 'percentage', + 'percent_price': 15, + })], + }) + + env['product.pricelist'].create({ + 'name': 'Pricelist base 2', + 'item_ids': [(0, 0, { + 'base': 'pricelist', + 'base_pricelist_id': pricelist_base_pricelist.id, + 'compute_price': 'percentage', + 'percent_price': 3, + })], + }) + + env['product.pricelist'].create({ + 'name': 'Pricelist base rounding', + 'item_ids': [(0, 0, { + 'base': 'pricelist', + 'base_pricelist_id': fixed_pricelist.id, + 'compute_price': 'percentage', + 'percent_price': 0.01, + })], + }) + + excluded_pricelist = env['product.pricelist'].create({ + 'name': 'Not loaded' + }) + res_partner_18 = self.env['res.partner'].create({ + 'name': 'Lumber Inc', + 'is_company': True, + }) + res_partner_18.property_product_pricelist = excluded_pricelist + + partner = self.env['res.partner'].create({ + 'name': 'TEST PARTNER', + 'email': 'test@partner.com', + }) + + # set the company currency to USD, otherwise it will assume + # euro's. this will cause issues as the sales journal is in + # USD, because of this all products would have a different + # price + main_company.currency_id = env.ref('base.USD') + + test_sale_journal = journal_obj.create({'name': 'Sales Journal - Test', + 'code': 'TSJ', + 'type': 'sale', + 'company_id': main_company.id}) + + all_pricelists = env['product.pricelist'].search([('id', '!=', excluded_pricelist.id)]) + all_pricelists.write(dict(currency_id=main_company.currency_id.id)) + + src_tax = env['account.tax'].create({'name': "SRC", 'amount': 10}) + dst_tax = env['account.tax'].create({'name': "DST", 'amount': 5}) + + letter_tray.taxes_id = [(6, 0, [src_tax.id])] + + self.main_pos_config.write({ + 'tax_regime_selection': True, + 'fiscal_position_ids': [(0, 0, { + 'name': "FP-POS-2M", + 'tax_ids': [ + (0,0,{'tax_src_id': src_tax.id, + 'tax_dest_id': src_tax.id}), + (0,0,{'tax_src_id': src_tax.id, + 'tax_dest_id': dst_tax.id})] + })], + 'journal_id': test_sale_journal.id, + 'invoice_journal_id': test_sale_journal.id, + 'payment_method_ids': [(0, 0, { 'name': 'Cash', + 'is_cash_count': True, + 'cash_journal_id': cash_journal.id, + 'receivable_account_id': account_receivable.id, + })], + 'use_pricelist': True, + 'pricelist_id': public_pricelist.id, + 'available_pricelist_ids': [(4, pricelist.id) for pricelist in all_pricelists], + 'module_pos_loyalty': False, + }) + + # Change the default sale pricelist of customers, + # so the js tests can expect deterministically this pricelist when selecting a customer. + env['ir.property']._set_default( + "property_product_pricelist", + "res.partner", + public_pricelist, + ) + + +@odoo.tests.tagged('post_install', '-at_install') +class TestUi(TestPointOfSaleHttpCommon): + def test_01_pos_basic_order(self): + + self.main_pos_config.write({ + 'iface_tipproduct': True, + 'tip_product_id': self.tip.id, + }) + + # open a session, the /pos/ui controller will redirect to it + self.main_pos_config.open_session_cb(check_coa=False) + + # needed because tests are run before the module is marked as + # installed. In js web will only load qweb coming from modules + # that are returned by the backend in module_boot. Without + # this you end up with js, css but no qweb. + self.env['ir.module.module'].search([('name', '=', 'point_of_sale')], limit=1).state = 'installed' + + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'pos_pricelist', login="admin") + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'pos_basic_order', login="admin") + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ProductScreenTour', login="admin") + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenTour', login="admin") + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ReceiptScreenTour', login="admin") + + for order in self.env['pos.order'].search([]): + self.assertEqual(order.state, 'paid', "Validated order has payment of " + str(order.amount_paid) + " and total of " + str(order.amount_total)) + + # check if email from ReceiptScreenTour is properly sent + email_count = self.env['mail.mail'].search_count([('email_to', '=', 'test@receiptscreen.com')]) + self.assertEqual(email_count, 1) + + def test_02_pos_with_invoiced(self): + self.main_pos_config.open_session_cb(check_coa=False) + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ChromeTour', login="admin") + n_invoiced = self.env['pos.order'].search_count([('state', '=', 'invoiced')]) + n_paid = self.env['pos.order'].search_count([('state', '=', 'paid')]) + self.assertEqual(n_invoiced, 1, 'There should be 1 invoiced order.') + self.assertEqual(n_paid, 2, 'There should be 2 paid order.') + + def test_03_order_management(self): + self.main_pos_config.write({ 'manage_orders': True, 'module_account': True }) + self.main_pos_config.open_session_cb(check_coa=False) + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'OrderManagementScreenTour', login="admin") + + def test_04_product_configurator(self): + self.main_pos_config.write({ 'product_configurator': True }) + self.main_pos_config.open_session_cb(check_coa=False) + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config, 'ProductConfiguratorTour', login="admin") + + def test_05_ticket_screen(self): + self.main_pos_config.open_session_cb(check_coa=False) + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'TicketScreenTour', login="admin") diff --git a/addons/point_of_sale/tests/test_js.py b/addons/point_of_sale/tests/test_js.py new file mode 100644 index 00000000..560e4d40 --- /dev/null +++ b/addons/point_of_sale/tests/test_js.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo.addons.web.tests.test_js +import odoo.tests + + +@odoo.tests.tagged("post_install", "-at_install") +class WebSuite(odoo.tests.HttpCase): + def setUp(self): + super().setUp() + env = self.env(user=self.env.ref('base.user_admin')) + self.main_pos_config = env.ref('point_of_sale.pos_config_main') + + def test_pos_js(self): + # open a session, the /pos/ui controller will redirect to it + self.main_pos_config.open_session_cb(check_coa=False) + + # point_of_sale desktop test suite + self.browser_js( + "/pos/ui/tests?mod=web&failfast", "", "", login="admin", timeout=1800 + ) diff --git a/addons/point_of_sale/tests/test_point_of_sale.py b/addons/point_of_sale/tests/test_point_of_sale.py new file mode 100644 index 00000000..83cf6c0c --- /dev/null +++ b/addons/point_of_sale/tests/test_point_of_sale.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase + + +class TestPointOfSale(TransactionCase): + def setUp(self): + super(TestPointOfSale, self).setUp() + + # ignore pre-existing pricelists for the purpose of this test + self.env["product.pricelist"].search([]).write({"active": False}) + + self.currency = self.env.ref("base.USD") + self.company1 = self.env["res.company"].create({ + "name": "company 1", + "currency_id": self.currency.id + }) + self.company2 = self.env["res.company"].create({ + "name": "company 2", + "currency_id": self.currency.id + }) + self.company2_pricelist = self.env["product.pricelist"].create({ + "name": "company 2 pricelist", + "currency_id": self.currency.id, + "company_id": self.company2.id, + "sequence": 1, # force this pricelist to be first + }) + + self.env.user.company_id = self.company1 + + def test_default_pricelist_with_company(self): + """ Verify that the default pricelist belongs to the same company as the config """ + company1_pricelist = self.env["product.pricelist"].create({ + "name": "company 1 pricelist", + "currency_id": self.currency.id, + "company_id": self.company1.id, + "sequence": 2, + }) + + # make sure this doesn't pick the company2 pricelist + new_config = self.env["pos.config"].create({ + "name": "usd config" + }) + + self.assertEqual(new_config.pricelist_id, company1_pricelist, + "POS config incorrectly has pricelist %s" % new_config.pricelist_id.display_name) + + def test_default_pricelist_without_company(self): + """ Verify that a default pricelist without a company works """ + universal_pricelist = self.env["product.pricelist"].create({ + "name": "universal pricelist", + "currency_id": self.currency.id, + "sequence": 2, + }) + + # make sure this doesn't pick the company2 pricelist + new_config = self.env["pos.config"].create({ + "name": "usd config" + }) + + self.assertEqual(new_config.pricelist_id, universal_pricelist, + "POS config incorrectly has pricelist %s" % new_config.pricelist_id.display_name) diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py new file mode 100644 index 00000000..52638474 --- /dev/null +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -0,0 +1,1008 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import time + +import odoo +from odoo import fields, tools +from odoo.tools import float_compare, mute_logger, test_reports +from odoo.tests.common import Form +from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon + + +@odoo.tests.tagged('post_install', '-at_install') +class TestPointOfSaleFlow(TestPointOfSaleCommon): + + def compute_tax(self, product, price, qty=1, taxes=None): + if not taxes: + taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id) + currency = self.pos_config.pricelist_id.currency_id + res = taxes.compute_all(price, currency, qty, product=product) + untax = res['total_excluded'] + return untax, sum(tax.get('amount', 0.0) for tax in res['taxes']) + + def test_order_refund(self): + self.pos_config.open_session_cb(check_coa=False) + current_session = self.pos_config.current_session_id + # I create a new PoS order with 2 lines + order = self.PosOrder.create({ + 'company_id': self.env.company.id, + 'session_id': current_session.id, + 'partner_id': self.partner1.id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 5.0, + 'qty': 2.0, + 'tax_ids': [(6, 0, self.product3.taxes_id.ids)], + 'price_subtotal': 450 * (1 - 5/100.0) * 2, + 'price_subtotal_incl': 450 * (1 - 5/100.0) * 2, + }), (0, 0, { + 'name': "OL/0002", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 5.0, + 'qty': 3.0, + 'tax_ids': [(6, 0, self.product4.taxes_id.ids)], + 'price_subtotal': 300 * (1 - 5/100.0) * 3, + 'price_subtotal_incl': 300 * (1 - 5/100.0) * 3, + })], + 'amount_total': 1710.0, + 'amount_tax': 0.0, + 'amount_paid': 0.0, + 'amount_return': 0.0, + }) + + payment_context = {"active_ids": order.ids, "active_id": order.id} + order_payment = self.PosMakePayment.with_context(**payment_context).create({ + 'amount': order.amount_total, + 'payment_method_id': self.cash_payment_method.id + }) + order_payment.with_context(**payment_context).check() + self.assertAlmostEqual(order.amount_total, order.amount_paid, msg='Order should be fully paid.') + + # I create a refund + refund_action = order.refund() + refund = self.PosOrder.browse(refund_action['res_id']) + + self.assertEqual(order.amount_total, -1*refund.amount_total, + "The refund does not cancel the order (%s and %s)" % (order.amount_total, refund.amount_total)) + + payment_context = {"active_ids": refund.ids, "active_id": refund.id} + refund_payment = self.PosMakePayment.with_context(**payment_context).create({ + 'amount': refund.amount_total, + 'payment_method_id': self.cash_payment_method.id, + }) + + # I click on the validate button to register the payment. + refund_payment.with_context(**payment_context).check() + + self.assertEqual(refund.state, 'paid', "The refund is not marked as paid") + self.assertTrue(refund.payment_ids.payment_method_id.is_cash_count, msg='There should only be one payment and paid in cash.') + + current_session.action_pos_session_closing_control() + self.assertEqual(current_session.state, 'closed', msg='State of current session should be closed.') + + def test_order_refund_lots(self): + # open pos session + self.pos_config.open_session_cb() + current_session = self.pos_config.current_session_id + + # set up product iwith SN tracing and create two lots (1001, 1002) + self.stock_location = self.company_data['default_warehouse'].lot_stock_id + self.product2 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'serial', + 'categ_id': self.env.ref('product.product_category_all').id, + }) + + inventory = self.env['stock.inventory'].create({ + 'name': 'add product2', + 'location_ids': [(4, self.stock_location.id)], + 'product_ids': [(4, self.product2.id)], + }) + inventory.action_start() + + lot1 = self.env['stock.production.lot'].create({ + 'name': '1001', + 'product_id': self.product2.id, + 'company_id': self.env.company.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': '1002', + 'product_id': self.product2.id, + 'company_id': self.env.company.id, + }) + + self.env['stock.inventory.line'].create([ + { + 'inventory_id': inventory.id, + 'location_id': self.stock_location.id, + 'product_id': self.product2.id, + 'prod_lot_id': lot1.id, + 'product_qty': 1 + }, + { + 'inventory_id': inventory.id, + 'location_id': self.stock_location.id, + 'product_id': self.product2.id, + 'prod_lot_id': lot2.id, + 'product_qty': 1 + }, + ]) + + inventory.action_validate() + + # create pos order with the two SN created before + + order = self.PosOrder.create({ + 'company_id': self.env.company.id, + 'session_id': current_session.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'id': 1, + 'product_id': self.product2.id, + 'price_unit': 6, + 'discount': 0, + 'qty': 2, + 'tax_ids': [[6, False, []]], + 'price_subtotal': 12, + 'price_subtotal_incl': 12, + 'pack_lot_ids': [ + [0, 0, {'lot_name': '1001'}], + [0, 0, {'lot_name': '1002'}], + ] + })], + 'pricelist_id': 1, + 'amount_paid': 12.0, + 'amount_total': 12.0, + 'amount_tax': 0.0, + 'amount_return': 0.0, + 'to_invoice': False, + }) + + payment_context = {"active_ids": order.ids, "active_id": order.id} + order_payment = self.PosMakePayment.with_context(**payment_context).create({ + 'amount': order.amount_total, + 'payment_method_id': self.cash_payment_method.id + }) + order_payment.with_context(**payment_context).check() + + # I create a refund + refund_action = order.refund() + refund = self.PosOrder.browse(refund_action['res_id']) + + order_lot_id = [lot_id.lot_name for lot_id in order.lines.pack_lot_ids] + refund_lot_id = [lot_id.lot_name for lot_id in refund.lines.pack_lot_ids] + self.assertEqual( + order_lot_id, + refund_lot_id, + "In the refund we should find the same lot as in the original order") + + payment_context = {"active_ids": refund.ids, "active_id": refund.id} + refund_payment = self.PosMakePayment.with_context(**payment_context).create({ + 'amount': refund.amount_total, + 'payment_method_id': self.cash_payment_method.id, + }) + + # I click on the validate button to register the payment. + refund_payment.with_context(**payment_context).check() + + self.assertEqual(refund.state, 'paid', "The refund is not marked as paid") + current_session.action_pos_session_closing_control() + + def test_order_to_picking(self): + """ + In order to test the Point of Sale in module, I will do three orders from the sale to the payment, + invoicing + picking, but will only check the picking consistency in the end. + + TODO: Check the negative picking after changing the picking relation to One2many (also for a mixed use case), + check the quantity, the locations and return picking logic + """ + + # I click on create a new session button + self.pos_config.open_session_cb(check_coa=False) + current_session = self.pos_config.current_session_id + + # I create a PoS order with 2 units of PCSC234 at 450 EUR + # and 3 units of PCSC349 at 300 EUR. + untax1, atax1 = self.compute_tax(self.product3, 450, 2) + untax2, atax2 = self.compute_tax(self.product4, 300, 3) + self.pos_order_pos1 = self.PosOrder.create({ + 'company_id': self.env.company.id, + 'session_id': current_session.id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': 2.0, + 'tax_ids': [(6, 0, self.product3.taxes_id.ids)], + 'price_subtotal': untax1, + 'price_subtotal_incl': untax1 + atax1, + }), (0, 0, { + 'name': "OL/0002", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 0.0, + 'qty': 3.0, + 'tax_ids': [(6, 0, self.product4.taxes_id.ids)], + 'price_subtotal': untax2, + 'price_subtotal_incl': untax2 + atax2, + })], + 'amount_tax': atax1 + atax2, + 'amount_total': untax1 + untax2 + atax1 + atax2, + 'amount_paid': 0, + 'amount_return': 0, + }) + + context_make_payment = { + "active_ids": [self.pos_order_pos1.id], + "active_id": self.pos_order_pos1.id + } + self.pos_make_payment_2 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': untax1 + untax2 + atax1 + atax2 + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos1.id} + + self.pos_make_payment_2.with_context(context_payment).check() + # I check that the order is marked as paid + self.assertEqual( + self.pos_order_pos1.state, + 'paid', + 'Order should be in paid state.' + ) + + # I test that the pickings are created as expected during payment + # One picking attached and having all the positive move lines in the correct state + self.assertEqual( + self.pos_order_pos1.picking_ids[0].state, + 'done', + 'Picking should be in done state.' + ) + self.assertEqual( + self.pos_order_pos1.picking_ids[0].move_lines.mapped('state'), + ['done', 'done'], + 'Move Lines should be in done state.' + ) + + # I create a second order + untax1, atax1 = self.compute_tax(self.product3, 450, -2) + untax2, atax2 = self.compute_tax(self.product4, 300, -3) + self.pos_order_pos2 = self.PosOrder.create({ + 'company_id': self.env.company.id, + 'session_id': current_session.id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0003", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': (-2.0), + 'tax_ids': [(6, 0, self.product3.taxes_id.ids)], + 'price_subtotal': untax1, + 'price_subtotal_incl': untax1 + atax1, + }), (0, 0, { + 'name': "OL/0004", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 0.0, + 'qty': (-3.0), + 'tax_ids': [(6, 0, self.product4.taxes_id.ids)], + 'price_subtotal': untax2, + 'price_subtotal_incl': untax2 + atax2, + })], + 'amount_tax': atax1 + atax2, + 'amount_total': untax1 + untax2 + atax1 + atax2, + 'amount_paid': 0, + 'amount_return': 0, + }) + + context_make_payment = { + "active_ids": [self.pos_order_pos2.id], + "active_id": self.pos_order_pos2.id + } + self.pos_make_payment_3 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': untax1 + untax2 + atax1 + atax2 + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos2.id} + self.pos_make_payment_3.with_context(context_payment).check() + + # I check that the order is marked as paid + self.assertEqual( + self.pos_order_pos2.state, + 'paid', + 'Order should be in paid state.' + ) + + # I test that the pickings are created as expected + # One picking attached and having all the positive move lines in the correct state + self.assertEqual( + self.pos_order_pos2.picking_ids[0].state, + 'done', + 'Picking should be in done state.' + ) + self.assertEqual( + self.pos_order_pos2.picking_ids[0].move_lines.mapped('state'), + ['done', 'done'], + 'Move Lines should be in done state.' + ) + + untax1, atax1 = self.compute_tax(self.product3, 450, -2) + untax2, atax2 = self.compute_tax(self.product4, 300, 3) + self.pos_order_pos3 = self.PosOrder.create({ + 'company_id': self.env.company.id, + 'session_id': current_session.id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0005", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': (-2.0), + 'tax_ids': [(6, 0, self.product3.taxes_id.ids)], + 'price_subtotal': untax1, + 'price_subtotal_incl': untax1 + atax1, + }), (0, 0, { + 'name': "OL/0006", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 0.0, + 'qty': 3.0, + 'tax_ids': [(6, 0, self.product4.taxes_id.ids)], + 'price_subtotal': untax2, + 'price_subtotal_incl': untax2 + atax2, + })], + 'amount_tax': atax1 + atax2, + 'amount_total': untax1 + untax2 + atax1 + atax2, + 'amount_paid': 0, + 'amount_return': 0, + }) + + context_make_payment = { + "active_ids": [self.pos_order_pos3.id], + "active_id": self.pos_order_pos3.id + } + self.pos_make_payment_4 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': untax1 + untax2 + atax1 + atax2, + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos3.id} + self.pos_make_payment_4.with_context(context_payment).check() + + # I check that the order is marked as paid + self.assertEqual( + self.pos_order_pos3.state, + 'paid', + 'Order should be in paid state.' + ) + + # I test that the pickings are created as expected + # One picking attached and having all the positive move lines in the correct state + self.assertEqual( + self.pos_order_pos3.picking_ids[0].state, + 'done', + 'Picking should be in done state.' + ) + self.assertEqual( + self.pos_order_pos3.picking_ids[0].move_lines.mapped('state'), + ['done'], + 'Move Lines should be in done state.' + ) + # I close the session to generate the journal entries + self.pos_config.current_session_id.action_pos_session_closing_control() + + def test_order_to_picking02(self): + """ This test is similar to test_order_to_picking except that this time, there are two products: + - One tracked by lot + - One untracked + - Both are in a sublocation of the main warehouse + """ + tracked_product, untracked_product = self.env['product.product'].create([{ + 'name': 'SuperProduct Tracked', + 'type': 'product', + 'tracking': 'lot', + 'available_in_pos': True, + }, { + 'name': 'SuperProduct Untracked', + 'type': 'product', + 'available_in_pos': True, + }]) + wh_location = self.company_data['default_warehouse'].lot_stock_id + shelf1_location = self.env['stock.location'].create({ + 'name': 'shelf1', + 'usage': 'internal', + 'location_id': wh_location.id, + }) + lot = self.env['stock.production.lot'].create({ + 'name': 'SuperLot', + 'product_id': tracked_product.id, + 'company_id': self.env.company.id, + }) + qty = 2 + self.env['stock.quant']._update_available_quantity(tracked_product, shelf1_location, qty, lot_id=lot) + self.env['stock.quant']._update_available_quantity(untracked_product, shelf1_location, qty) + + self.pos_config.open_session_cb() + self.pos_config.current_session_id.update_stock_at_closing = False + + untax, atax = self.compute_tax(tracked_product, 1.15, 1) + + for i in range(qty): + pos_order = self.PosOrder.create({ + 'company_id': self.env.company.id, + 'session_id': self.pos_config.current_session_id.id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': tracked_product.id, + 'price_unit': untax + atax, + 'discount': 0.0, + 'qty': 1.0, + 'tax_ids': [(6, 0, tracked_product.taxes_id.ids)], + 'price_subtotal': untax, + 'price_subtotal_incl': untax + atax, + 'pack_lot_ids': [[0, 0, {'lot_name': lot.name}]], + }), (0, 0, { + 'name': "OL/0002", + 'product_id': untracked_product.id, + 'price_unit': untax + atax, + 'discount': 0.0, + 'qty': 1.0, + 'tax_ids': [(6, 0, untracked_product.taxes_id.ids)], + 'price_subtotal': untax, + 'price_subtotal_incl': untax + atax, + })], + 'amount_tax': 2 * atax, + 'amount_total': 2 * (untax + atax), + 'amount_paid': 0, + 'amount_return': 0, + }) + + context_make_payment = { + "active_ids": [pos_order.id], + "active_id": pos_order.id, + } + pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 2 * (untax + atax), + }) + context_payment = {'active_id': pos_order.id} + pos_make_payment.with_context(context_payment).check() + + self.assertEqual(pos_order.state, 'paid') + self.assertEqual(pos_order.picking_ids.move_line_ids[0].lot_id, lot) + self.assertFalse(pos_order.picking_ids.move_line_ids[1].lot_id) + self.assertEqual(pos_order.picking_ids.move_line_ids[0].location_id, shelf1_location) + self.assertEqual(pos_order.picking_ids.move_line_ids[1].location_id, shelf1_location) + + self.pos_config.current_session_id.action_pos_session_closing_control() + + def test_order_to_invoice(self): + + self.pos_config.open_session_cb(check_coa=False) + current_session = self.pos_config.current_session_id + + untax1, atax1 = self.compute_tax(self.product3, 450*0.95, 2) + untax2, atax2 = self.compute_tax(self.product4, 300*0.95, 3) + # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) + self.pos_order_pos1 = self.PosOrder.create({ + 'company_id': self.env.company.id, + 'session_id': current_session.id, + 'partner_id': self.partner1.id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 5.0, + 'qty': 2.0, + 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], + 'price_subtotal': untax1, + 'price_subtotal_incl': untax1 + atax1, + }), (0, 0, { + 'name': "OL/0002", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 5.0, + 'qty': 3.0, + 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], + 'price_subtotal': untax2, + 'price_subtotal_incl': untax2 + atax2, + })], + 'amount_tax': atax1 + atax2, + 'amount_total': untax1 + untax2 + atax1 + atax2, + 'amount_paid': 0.0, + 'amount_return': 0.0, + }) + + # I click on the "Make Payment" wizard to pay the PoS order + context_make_payment = {"active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id} + self.pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': untax1 + untax2 + atax1 + atax2, + }) + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos1.id} + self.pos_make_payment.with_context(context_payment).check() + + # I check that the order is marked as paid and there is no invoice + # attached to it + self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.") + self.assertFalse(self.pos_order_pos1.account_move, 'Invoice should not be attached to order.') + + # I generate an invoice from the order + res = self.pos_order_pos1.action_pos_order_invoice() + self.assertIn('res_id', res, "Invoice should be created") + + # I test that the total of the attached invoice is correct + invoice = self.env['account.move'].browse(res['res_id']) + if invoice.state != 'posted': + invoice.action_post() + self.assertAlmostEqual( + invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct") + + # I close the session to generate the journal entries + current_session.action_pos_session_closing_control() + + """In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports""" + + # I select the period and journal for the bank statement + + context_journal = {'journal_type': 'bank'} + self.assertTrue(self.AccountBankStatement.with_context( + context_journal)._default_journal(), 'Journal has not been selected') + journal = self.env['account.journal'].create({ + 'name': 'Bank Test', + 'code': 'BNKT', + 'type': 'bank', + 'company_id': self.env.company.id, + }) + # I create a bank statement with Opening and Closing balance 0. + account_statement = self.AccountBankStatement.create({ + 'balance_start': 0.0, + 'balance_end_real': 0.0, + 'date': time.strftime('%Y-%m-%d'), + 'journal_id': journal.id, + 'company_id': self.env.company.id, + 'name': 'pos session test', + }) + # I create bank statement line + account_statement_line = self.AccountBankStatementLine.create({ + 'amount': 1000, + 'partner_id': self.partner4.id, + 'statement_id': account_statement.id, + 'payment_ref': 'EXT001' + }) + # I modify the bank statement and set the Closing Balance. + account_statement.write({ + 'balance_end_real': 1000.0, + }) + + # I reconcile the bank statement. + new_aml_dicts = [{ + 'account_id': self.partner4.property_account_receivable_id.id, + 'name': "EXT001", + 'credit': 1000.0, + 'debit': 0.0, + }] + + # I confirm the bank statement using Confirm button + + self.AccountBankStatement.button_validate() + + def test_create_from_ui(self): + """ + Simulation of sales coming from the interface, even after closing the session + """ + + # I click on create a new session button + self.pos_config.open_session_cb(check_coa=False) + + current_session = self.pos_config.current_session_id + num_starting_orders = len(current_session.order_ids) + + untax, atax = self.compute_tax(self.led_lamp, 0.9) + carrot_order = {'data': + {'amount_paid': untax + atax, + 'amount_return': 0, + 'amount_tax': atax, + 'amount_total': untax + atax, + 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), + 'fiscal_position_id': False, + 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 42, + 'pack_lot_ids': [], + 'price_unit': 0.9, + 'product_id': self.led_lamp.id, + 'price_subtotal': 0.9, + 'price_subtotal_incl': 1.04, + 'qty': 1, + 'tax_ids': [(6, 0, self.led_lamp.taxes_id.ids)]}]], + 'name': 'Order 00042-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': 2, + 'statement_ids': [[0, + 0, + {'amount': untax + atax, + 'name': fields.Datetime.now(), + 'payment_method_id': self.cash_payment_method.id}]], + 'uid': '00042-003-0014', + 'user_id': self.env.uid}, + 'id': '00042-003-0014', + 'to_invoice': False} + + untax, atax = self.compute_tax(self.whiteboard_pen, 1.2) + zucchini_order = {'data': + {'amount_paid': untax + atax, + 'amount_return': 0, + 'amount_tax': atax, + 'amount_total': untax + atax, + 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), + 'fiscal_position_id': False, + 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 3, + 'pack_lot_ids': [], + 'price_unit': 1.2, + 'product_id': self.whiteboard_pen.id, + 'price_subtotal': 1.2, + 'price_subtotal_incl': 1.38, + 'qty': 1, + 'tax_ids': [(6, 0, self.whiteboard_pen.taxes_id.ids)]}]], + 'name': 'Order 00043-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': self.pos_config.journal_id.id, + 'statement_ids': [[0, + 0, + {'amount': untax + atax, + 'name': fields.Datetime.now(), + 'payment_method_id': self.credit_payment_method.id}]], + 'uid': '00043-003-0014', + 'user_id': self.env.uid}, + 'id': '00043-003-0014', + 'to_invoice': False} + + untax, atax = self.compute_tax(self.newspaper_rack, 1.28) + newspaper_rack_order = {'data': + {'amount_paid': untax + atax, + 'amount_return': 0, + 'amount_tax': atax, + 'amount_total': untax + atax, + 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), + 'fiscal_position_id': False, + 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 3, + 'pack_lot_ids': [], + 'price_unit': 1.28, + 'product_id': self.newspaper_rack.id, + 'price_subtotal': 1.28, + 'price_subtotal_incl': 1.47, + 'qty': 1, + 'tax_ids': [[6, False, self.newspaper_rack.taxes_id.ids]]}]], + 'name': 'Order 00044-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': self.pos_config.journal_id.id, + 'statement_ids': [[0, + 0, + {'amount': untax + atax, + 'name': fields.Datetime.now(), + 'payment_method_id': self.bank_payment_method.id}]], + 'uid': '00044-003-0014', + 'user_id': self.env.uid}, + 'id': '00044-003-0014', + 'to_invoice': False} + + # I create an order on an open session + self.PosOrder.create_from_ui([carrot_order]) + self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Submitted order not encoded") + + # I close the session + current_session.action_pos_session_closing_control() + self.assertEqual(current_session.state, 'closed', "Session was not properly closed") + self.assertFalse(self.pos_config.current_session_id, "Current session not properly recomputed") + + # I keep selling after the session is closed + with mute_logger('odoo.addons.point_of_sale.models.pos_order'): + self.PosOrder.create_from_ui([zucchini_order, newspaper_rack_order]) + rescue_session = self.PosSession.search([ + ('config_id', '=', self.pos_config.id), + ('state', '=', 'opened'), + ('rescue', '=', True) + ]) + self.assertEqual(len(rescue_session), 1, "One (and only one) rescue session should be created for orphan orders") + self.assertIn("(RESCUE FOR %s)" % current_session.name, rescue_session.name, "Rescue session is not linked to the previous one") + self.assertEqual(len(rescue_session.order_ids), 2, "Rescue session does not contain both orders") + + # I close the rescue session + rescue_session.action_pos_session_closing_control() + self.assertEqual(rescue_session.state, 'closed', "Rescue session was not properly closed") + + def test_order_to_payment_currency(self): + """ + In order to test the Point of Sale in module, I will do a full flow from the sale to the payment and invoicing. + I will use two products, one with price including a 10% tax, the other one with 5% tax excluded from the price. + The order will be in a different currency than the company currency. + """ + # Make sure the company is in USD + self.env.cr.execute( + "UPDATE res_company SET currency_id = %s WHERE id = %s", + [self.env.ref('base.USD').id, self.env.company.id]) + + # Demo data are crappy, clean-up the rates + self.env['res.currency.rate'].search([]).unlink() + self.env['res.currency.rate'].create({ + 'name': '2010-01-01', + 'rate': 2.0, + 'currency_id': self.env.ref('base.EUR').id, + }) + + # make a config that has currency different from the company + eur_pricelist = self.partner1.property_product_pricelist.copy(default={'currency_id': self.env.ref('base.EUR').id}) + sale_journal = self.env['account.journal'].create({ + 'name': 'PoS Sale EUR', + 'type': 'sale', + 'code': 'POSE', + 'company_id': self.company.id, + 'sequence': 12, + 'currency_id': self.env.ref('base.EUR').id + }) + eur_config = self.pos_config.create({ + 'name': 'Shop EUR Test', + 'module_account': False, + 'journal_id': sale_journal.id, + 'use_pricelist': True, + 'available_pricelist_ids': [(6, 0, eur_pricelist.ids)], + 'pricelist_id': eur_pricelist.id, + 'payment_method_ids': [(6, 0, self.bank_payment_method.ids)] + }) + + # I click on create a new session button + eur_config.open_session_cb(check_coa=False) + current_session = eur_config.current_session_id + + # I create a PoS order with 2 units of PCSC234 at 450 EUR (Tax Incl) + # and 3 units of PCSC349 at 300 EUR. (Tax Excl) + + untax1, atax1 = self.compute_tax(self.product3, 450, 2) + untax2, atax2 = self.compute_tax(self.product4, 300, 3) + self.pos_order_pos0 = self.PosOrder.create({ + 'company_id': self.env.company.id, + 'session_id': current_session.id, + 'pricelist_id': eur_pricelist.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': 2.0, + 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id == self.env.company).ids)], + 'price_subtotal': untax1, + 'price_subtotal_incl': untax1 + atax1, + }), (0, 0, { + 'name': "OL/0002", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 0.0, + 'qty': 3.0, + 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id == self.env.company).ids)], + 'price_subtotal': untax2, + 'price_subtotal_incl': untax2 + atax2, + })], + 'amount_tax': atax1 + atax2, + 'amount_total': untax1 + untax2 + atax1 + atax2, + 'amount_paid': 0.0, + 'amount_return': 0.0, + }) + + # I check that the total of the order is now equal to (450*2 + + # 300*3*1.05)*0.95 + self.assertLess( + abs(self.pos_order_pos0.amount_total - (450 * 2 + 300 * 3 * 1.05)), + 0.01, 'The order has a wrong total including tax and discounts') + + # I click on the "Make Payment" wizard to pay the PoS order with a + # partial amount of 100.0 EUR + context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} + self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 100.0, + 'payment_method_id': self.bank_payment_method.id, + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos0.id} + self.pos_make_payment_0.with_context(context_payment).check() + + # I check that the order is not marked as paid yet + self.assertEqual(self.pos_order_pos0.state, 'draft', 'Order should be in draft state.') + + # On the second payment proposition, I check that it proposes me the + # remaining balance which is 1790.0 EUR + defs = self.pos_make_payment_0.with_context({'active_id': self.pos_order_pos0.id}).default_get(['amount']) + + self.assertLess( + abs(defs['amount'] - ((450 * 2 + 300 * 3 * 1.05) - 100.0)), 0.01, "The remaining balance is incorrect.") + + #'I pay the remaining balance. + context_make_payment = { + "active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} + + self.pos_make_payment_1 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': (450 * 2 + 300 * 3 * 1.05) - 100.0, + 'payment_method_id': self.bank_payment_method.id, + }) + + # I click on the validate button to register the payment. + self.pos_make_payment_1.with_context(context_make_payment).check() + + # I check that the order is marked as paid + self.assertEqual(self.pos_order_pos0.state, 'paid', 'Order should be in paid state.') + + # I generate the journal entries + current_session.action_pos_session_validate() + + # I test that the generated journal entry is attached to the PoS order + self.assertTrue(current_session.move_id, "Journal entry should have been attached to the session.") + + # Check the amounts + debit_lines = current_session.move_id.mapped('line_ids.debit') + credit_lines = current_session.move_id.mapped('line_ids.credit') + amount_currency_lines = current_session.move_id.mapped('line_ids.amount_currency') + for a, b in zip(sorted(debit_lines), [0.0, 0.0, 0.0, 0.0, 922.5]): + self.assertAlmostEqual(a, b) + for a, b in zip(sorted(credit_lines), [0.0, 22.5, 40.91, 409.09, 450]): + self.assertAlmostEqual(a, b) + for a, b in zip(sorted(amount_currency_lines), [-900, -818.18, -81.82, -45, 1845]): + self.assertAlmostEqual(a, b) + + def test_order_to_invoice_no_tax(self): + self.pos_config.open_session_cb(check_coa=False) + current_session = self.pos_config.current_session_id + + # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) + self.pos_order_pos1 = self.PosOrder.create({ + 'company_id': self.env.company.id, + 'session_id': current_session.id, + 'partner_id': self.partner1.id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 5.0, + 'qty': 2.0, + 'price_subtotal': 855, + 'price_subtotal_incl': 855, + }), (0, 0, { + 'name': "OL/0002", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 5.0, + 'qty': 3.0, + 'price_subtotal': 855, + 'price_subtotal_incl': 855, + })], + 'amount_tax': 855 * 2, + 'amount_total': 855 * 2, + 'amount_paid': 0.0, + 'amount_return': 0.0, + }) + + # I click on the "Make Payment" wizard to pay the PoS order + context_make_payment = {"active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id} + self.pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 855 * 2, + }) + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos1.id} + self.pos_make_payment.with_context(context_payment).check() + + # I check that the order is marked as paid and there is no invoice + # attached to it + self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.") + self.assertFalse(self.pos_order_pos1.account_move, 'Invoice should not be attached to order yet.') + + # I generate an invoice from the order + res = self.pos_order_pos1.action_pos_order_invoice() + self.assertIn('res_id', res, "No invoice created") + + # I test that the total of the attached invoice is correct + invoice = self.env['account.move'].browse(res['res_id']) + if invoice.state != 'posted': + invoice.action_post() + self.assertAlmostEqual( + invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct") + + for iline in invoice.invoice_line_ids: + self.assertFalse(iline.tax_ids) + + self.pos_config.current_session_id.action_pos_session_closing_control() + + def test_order_with_deleted_tax(self): + # create tax + dummy_50_perc_tax = self.env['account.tax'].create({ + 'name': 'Tax 50%', + 'amount_type': 'percent', + 'amount': 50.0, + 'price_include': 0 + }) + + # set tax to product + product5 = self.env['product.product'].create({ + 'name': 'product5', + 'type': 'product', + 'categ_id': self.env.ref('product.product_category_all').id, + 'taxes_id': dummy_50_perc_tax.ids + }) + + # sell product thru pos + self.pos_config.open_session_cb(check_coa=False) + pos_session = self.pos_config.current_session_id + untax, atax = self.compute_tax(product5, 10.0) + product5_order = {'data': + {'amount_paid': untax + atax, + 'amount_return': 0, + 'amount_tax': atax, + 'amount_total': untax + atax, + 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), + 'fiscal_position_id': False, + 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 42, + 'pack_lot_ids': [], + 'price_unit': 10.0, + 'product_id': product5.id, + 'price_subtotal': 10.0, + 'price_subtotal_incl': 15.0, + 'qty': 1, + 'tax_ids': [(6, 0, product5.taxes_id.ids)]}]], + 'name': 'Order 12345-123-1234', + 'partner_id': False, + 'pos_session_id': pos_session.id, + 'sequence_number': 2, + 'statement_ids': [[0, + 0, + {'amount': untax + atax, + 'name': fields.Datetime.now(), + 'payment_method_id': self.cash_payment_method.id}]], + 'uid': '12345-123-1234', + 'user_id': self.env.uid}, + 'id': '12345-123-1234', + 'to_invoice': False} + self.PosOrder.create_from_ui([product5_order]) + + # delete tax + dummy_50_perc_tax.unlink() + + # close session (should not fail here) + pos_session.action_pos_session_closing_control() + + # check the difference line + diff_line = pos_session.move_id.line_ids.filtered(lambda line: line.name == 'Difference at closing PoS session') + self.assertAlmostEqual(diff_line.credit, 5.0, msg="Missing amount of 5.0") diff --git a/addons/point_of_sale/tests/test_point_of_sale_ui.py b/addons/point_of_sale/tests/test_point_of_sale_ui.py new file mode 100644 index 00000000..cd04adcd --- /dev/null +++ b/addons/point_of_sale/tests/test_point_of_sale_ui.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import HttpCase, tagged +from odoo import tools + + +@tagged('post_install', '-at_install') +class TestUi(HttpCase): + + # Avoid "A Chart of Accounts is not yet installed in your current company." + # Everything is set up correctly even without installed CoA + @tools.mute_logger('odoo.http') + def test_01_point_of_sale_tour(self): + + self.start_tour("/web", 'point_of_sale_tour', login="admin") diff --git a/addons/point_of_sale/tests/test_pos_basic_config.py b/addons/point_of_sale/tests/test_pos_basic_config.py new file mode 100644 index 00000000..ac32a0c7 --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_basic_config.py @@ -0,0 +1,662 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo + +from odoo import tools +from odoo.addons.point_of_sale.tests.common import TestPoSCommon + + +@odoo.tests.tagged('post_install', '-at_install') +class TestPoSBasicConfig(TestPoSCommon): + """ Test PoS with basic configuration + + The tests contain base scenarios in using pos. + More specialized cases are tested in other tests. + """ + + def setUp(self): + super(TestPoSBasicConfig, self).setUp() + self.config = self.basic_config + self.product0 = self.create_product('Product 0', self.categ_basic, 0.0, 0.0) + 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.product4 = self.create_product('Product_4', self.categ_basic, 9.96, 4.98) + self.adjust_inventory([self.product1, self.product2, self.product3], [100, 50, 50]) + + def test_orders_no_invoiced(self): + """ Test for orders without invoice + + 3 orders + - first 2 orders with cash payment + - last order with bank payment + + Orders + ====== + +---------+----------+-----------+----------+-----+-------+ + | order | payments | invoiced? | product | qty | total | + +---------+----------+-----------+----------+-----+-------+ + | order 1 | cash | no | product1 | 10 | 100 | + | | | | product2 | 5 | 100 | + +---------+----------+-----------+----------+-----+-------+ + | order 2 | cash | no | product2 | 7 | 140 | + | | | | product3 | 1 | 30 | + +---------+----------+-----------+----------+-----+-------+ + | order 3 | bank | no | product1 | 1 | 10 | + | | | | product2 | 3 | 60 | + | | | | product3 | 5 | 150 | + +---------+----------+-----------+----------+-----+-------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale | -590 | + | pos receivable cash | 370 | + | pos receivable bank | 220 | + +---------------------+---------+ + | Total balance | 0.0 | + +---------------------+---------+ + """ + start_qty_available = { + self.product1: self.product1.qty_available, + self.product2: self.product2.qty_available, + self.product3: self.product3.qty_available, + } + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product1, 10), (self.product2, 5)])) + orders.append(self.create_ui_order_data([(self.product2, 7), (self.product3, 1)])) + orders.append(self.create_ui_order_data( + [(self.product1, 1), (self.product3, 5), (self.product2, 3)], + payments=[(self.bank_pm, 220)] + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # check product qty_available after syncing the order + self.assertEqual( + self.product1.qty_available + 11, + start_qty_available[self.product1], + ) + self.assertEqual( + self.product2.qty_available + 15, + start_qty_available[self.product2], + ) + self.assertEqual( + self.product3.qty_available + 6, + start_qty_available[self.product3], + ) + + # picking and stock moves should be in done state + for order in self.pos_session.order_ids: + self.assertEqual( + order.picking_ids[0].state, + 'done', + 'Picking should be in done state.' + ) + move_lines = order.picking_ids[0].move_lines + self.assertEqual( + move_lines.mapped('state'), + ['done'] * len(move_lines), + 'Move Lines should be in done state.' + ) + + # close the session + self.pos_session.action_pos_session_validate() + + # check accounting values after the session is closed + session_move = self.pos_session.move_id + + sales_line = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sales_line.balance, -590.0, msg='Sales line balance should be equal to total orders amount.') + + receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertAlmostEqual(receivable_line_bank.balance, 220.0, msg='Bank receivable should be equal to the total bank payments.') + + receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 370.0, msg='Cash receivable should be equal to the total cash payments.') + + self.assertTrue(receivable_line_cash.full_reconcile_id, msg='Cash receivable line should be fully-reconciled.') + + def test_orders_with_invoiced(self): + """ Test for orders: one with invoice + + 3 orders + - order 1, paid by cash + - order 2, paid by bank + - order 3, paid by bank, invoiced + + Orders + ====== + +---------+----------+---------------+----------+-----+-------+ + | order | payments | invoiced? | product | qty | total | + +---------+----------+---------------+----------+-----+-------+ + | order 1 | cash | no | product1 | 6 | 60 | + | | | | product2 | 3 | 60 | + | | | | product3 | 1 | 30 | + +---------+----------+---------------+----------+-----+-------+ + | order 2 | bank | no | product1 | 1 | 10 | + | | | | product2 | 20 | 400 | + +---------+----------+---------------+----------+-----+-------+ + | order 3 | bank | yes, customer | product1 | 10 | 100 | + | | | | product3 | 1 | 30 | + +---------+----------+---------------+----------+-----+-------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale | -560 | + | pos receivable cash | 150 | + | pos receivable bank | 540 | + | receivable | -130 | + +---------------------+---------+ + | Total balance | 0.0 | + +---------------------+---------+ + """ + start_qty_available = { + self.product1: self.product1.qty_available, + self.product2: self.product2.qty_available, + self.product3: self.product3.qty_available, + } + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data( + [(self.product3, 1), (self.product1, 6), (self.product2, 3)], + payments=[(self.cash_pm, 150)], + )) + orders.append(self.create_ui_order_data( + [(self.product2, 20), (self.product1, 1)], + payments=[(self.bank_pm, 410)], + )) + orders.append(self.create_ui_order_data( + [(self.product1, 10), (self.product3, 1)], + payments=[(self.bank_pm, 130)], + customer=self.customer, + is_invoiced=True, + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # check product qty_available after syncing the order + self.assertEqual( + self.product1.qty_available + 17, + start_qty_available[self.product1], + ) + self.assertEqual( + self.product2.qty_available + 23, + start_qty_available[self.product2], + ) + self.assertEqual( + self.product3.qty_available + 2, + start_qty_available[self.product3], + ) + + # picking and stock moves should be in done state + # no exception for invoiced orders + for order in self.pos_session.order_ids: + self.assertEqual( + order.picking_ids[0].state, + 'done', + 'Picking should be in done state.' + ) + move_lines = order.picking_ids[0].move_lines + self.assertEqual( + move_lines.mapped('state'), + ['done'] * len(move_lines), + 'Move Lines should be in done state.' + ) + + # check account move in the invoiced order + invoiced_order = self.pos_session.order_ids.filtered(lambda order: order.account_move) + self.assertEqual(1, len(invoiced_order), 'Only one order is invoiced in this test.') + invoice = invoiced_order.account_move + self.assertAlmostEqual(invoice.amount_total, 130, msg='Amount total should be 130. Product is untaxed.') + invoice_receivable_line = invoice.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + + # check state of orders before validating the session. + self.assertEqual('invoiced', invoiced_order.state, msg="state should be 'invoiced' for invoiced orders.") + uninvoiced_orders = self.pos_session.order_ids - invoiced_order + self.assertTrue( + all([order.state == 'paid' for order in uninvoiced_orders]), + msg="state should be 'paid' for uninvoiced orders before validating the session." + ) + + # close the session + self.pos_session.action_pos_session_validate() + + # check state of orders after validating the session. + self.assertTrue( + all([order.state == 'done' for order in uninvoiced_orders]), + msg="State should be 'done' for uninvoiced orders after validating the session." + ) + + # check values after the session is closed + session_move = self.pos_session.move_id + + sales_line = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sales_line.balance, -(orders_total - invoice.amount_total), msg='Sales line should be total order minus invoiced order.') + + pos_receivable_line_bank = session_move.line_ids.filtered( + lambda line: self.bank_pm.name in line.name and line.account_id == self.bank_pm.receivable_account_id + ) + self.assertAlmostEqual(pos_receivable_line_bank.balance, 540.0, msg='Bank receivable should be equal to the total bank payments.') + + pos_receivable_line_cash = session_move.line_ids.filtered( + lambda line: self.cash_pm.name in line.name and line.account_id == self.bank_pm.receivable_account_id + ) + self.assertAlmostEqual(pos_receivable_line_cash.balance, 150.0, msg='Cash receivable should be equal to the total cash payments.') + + receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + self.assertAlmostEqual(receivable_line.balance, -invoice.amount_total) + + # cash receivable and invoice receivable lines should be fully reconciled + self.assertTrue(pos_receivable_line_cash.full_reconcile_id) + self.assertTrue(receivable_line.full_reconcile_id) + + # matching number of the receivable lines should be the same + self.assertEqual(receivable_line.full_reconcile_id, invoice_receivable_line.full_reconcile_id) + + def test_orders_with_zero_valued_invoiced(self): + """One invoiced order but with zero receivable line balance.""" + + self.open_new_session() + orders = [self.create_ui_order_data([(self.product0, 1)], payments=[(self.bank_pm, 0)], customer=self.customer, is_invoiced=True)] + self.env['pos.order'].create_from_ui(orders) + self.pos_session.action_pos_session_validate() + + invoice = self.pos_session.order_ids.account_move + invoice_receivable_line = invoice.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + receivable_line = self.pos_session.move_id.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + + self.assertTrue(invoice_receivable_line.reconciled) + self.assertTrue(receivable_line.reconciled) + + def test_return_order(self): + """ Test return order + + 2 orders + - 2nd order is returned + + Orders + ====== + +------------------+----------+-----------+----------+-----+-------+ + | order | payments | invoiced? | product | qty | total | + +------------------+----------+-----------+----------+-----+-------+ + | order 1 | bank | no | product1 | 1 | 10 | + | | | | product2 | 5 | 100 | + +------------------+----------+-----------+----------+-----+-------+ + | order 2 | cash | no | product1 | 3 | 30 | + | | | | product2 | 2 | 40 | + | | | | product3 | 1 | 30 | + +------------------+----------+-----------+----------+-----+-------+ + | order 3 (return) | cash | no | product1 | -3 | -30 | + | | | | product2 | -2 | -40 | + | | | | product3 | -1 | -30 | + +------------------+----------+-----------+----------+-----+-------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale (sales) | -210 | + | sale (refund) | 100 | + | pos receivable bank | 110 | + +---------------------+---------+ + | Total balance | 0.0 | + +---------------------+---------+ + """ + start_qty_available = { + self.product1: self.product1.qty_available, + self.product2: self.product2.qty_available, + self.product3: self.product3.qty_available, + } + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data( + [(self.product1, 1), (self.product2, 5)], + payments=[(self.bank_pm, 110)] + )) + orders.append(self.create_ui_order_data( + [(self.product1, 3), (self.product2, 2), (self.product3, 1)], + payments=[(self.cash_pm, 100)], + uid='12345-123-1234' + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(2, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # return order + order_to_return = self.pos_session.order_ids.filtered(lambda order: '12345-123-1234' in order.pos_reference) + order_to_return.refund() + refund_order = self.pos_session.order_ids.filtered(lambda order: order.state == 'draft') + + # check if amount to pay + self.assertAlmostEqual(refund_order.amount_total - refund_order.amount_paid, -100) + + # pay the refund + context_make_payment = {"active_ids": [refund_order.id], "active_id": refund_order.id} + make_payment = self.env['pos.make.payment'].with_context(context_make_payment).create({ + 'payment_method_id': self.cash_pm.id, + 'amount': -100, + }) + make_payment.check() + self.assertEqual(refund_order.state, 'paid', 'Payment is registered, order should be paid.') + self.assertAlmostEqual(refund_order.amount_paid, -100.0, msg='Amount paid for return order should be negative.') + + # check product qty_available after syncing the order + self.assertEqual( + self.product1.qty_available + 1, + start_qty_available[self.product1], + ) + self.assertEqual( + self.product2.qty_available + 5, + start_qty_available[self.product2], + ) + self.assertEqual( + self.product3.qty_available, + start_qty_available[self.product3], + ) + + # picking and stock moves should be in done state + # no exception of return orders + for order in self.pos_session.order_ids: + self.assertEqual( + order.picking_ids[0].state, + 'done', + 'Picking should be in done state.' + ) + move_lines = order.picking_ids[0].move_lines + self.assertEqual( + move_lines.mapped('state'), + ['done'] * len(move_lines), + 'Move Lines should be in done state.' + ) + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_move = self.pos_session.move_id + + sale_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertEqual(len(sale_lines), 2, msg='There should be lines for both sales and refund.') + self.assertAlmostEqual(sum(sale_lines.mapped('balance')), -110.0) + + receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertAlmostEqual(receivable_line_bank.balance, 110.0) + + # net cash in the session is zero, thus, there should be no receivable cash line. + receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertFalse(receivable_line_cash, 'There should be no receivable cash line because both the order and return order are paid with cash - they cancelled.') + + def test_split_cash_payments(self): + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data( + [(self.product1, 10), (self.product2, 5)], + payments=[(self.cash_split_pm, 100), (self.bank_pm, 100)] + )) + orders.append(self.create_ui_order_data( + [(self.product2, 7), (self.product3, 1)], + payments=[(self.cash_split_pm, 70), (self.bank_pm, 100)] + )) + orders.append(self.create_ui_order_data( + [(self.product1, 1), (self.product3, 5), (self.product2, 3)], + payments=[(self.cash_split_pm, 120), (self.bank_pm, 100)] + )) + + # 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 + account_move = self.pos_session.move_id + + bank_receivable_lines = account_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertEqual(len(bank_receivable_lines), 1, msg='Bank receivable lines should only have one line because it\'s supposed to be combined.') + self.assertAlmostEqual(bank_receivable_lines.balance, 300.0, msg='Bank receivable should be equal to the total bank payments.') + + cash_receivable_lines = account_move.line_ids.filtered(lambda line: self.cash_split_pm.name in line.name) + self.assertEqual(len(cash_receivable_lines), 3, msg='There should be a number of cash receivable lines because the cash_pm is `split_transactions`.') + self.assertAlmostEqual(sum(cash_receivable_lines.mapped('balance')), 290, msg='Total cash receivable balance should be equal to the total cash payments.') + + for line in cash_receivable_lines: + self.assertTrue(line.full_reconcile_id, msg='Each cash receivable line should be fully-reconciled.') + + def test_rounding_method(self): + # set the cash rounding method + self.config.cash_rounding = True + self.config.rounding_method = self.env['account.cash.rounding'].create({ + 'name': 'add_invoice_line', + 'rounding': 0.05, + 'strategy': 'add_invoice_line', + 'profit_account_id': self.company['default_cash_difference_income_account_id'].copy().id, + 'loss_account_id': self.company['default_cash_difference_expense_account_id'].copy().id, + 'rounding_method': 'HALF-UP', + }) + + self.open_new_session() + + """ Test for orders: one with invoice + + 3 orders + - order 1, paid by cash + - order 2, paid by bank + - order 3, paid by bank, invoiced + + Orders + ====== + +---------+----------+---------------+----------+-----+-------+ + | order | payments | invoiced? | product | qty | total | + +---------+----------+---------------+----------+-----+-------+ + | order 1 | bank | no | product1 | 6 | 60 | + | | | | product4 | 4 | 39.84 | + +---------+----------+---------------+----------+-----+-------+ + | order 2 | bank | yes | product4 | 3 | 29.88 | + | | | | product2 | 20 | 400 | + +---------+----------+---------------+----------+-----+-------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale | -596,56 | + | pos receivable bank | 516,64 | + | Rounding applied | -0,01 | + +---------------------+---------+ + | Total balance | 0.0 | + +---------------------+---------+ + """ + + # create orders + orders = [] + + # create orders + orders = [] + orders.append(self.create_ui_order_data( + [(self.product4, 3), (self.product2, 20)], + payments=[(self.bank_pm, 429.90)] + )) + + orders.append(self.create_ui_order_data( + [(self.product1, 6), (self.product4, 4)], + payments=[(self.bank_pm, 99.85)] + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + self.assertEqual(orders[0]['data']['amount_return'], 0, msg='The amount return should be 0') + self.assertEqual(orders[1]['data']['amount_return'], 0, msg='The amount return should be 0') + + # 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 + + rounding_line = session_account_move.line_ids.filtered(lambda line: line.name == 'Rounding line') + self.assertAlmostEqual(rounding_line.credit, 0.03, msg='The credit should be equals to 0.03') + + def test_correct_partner_on_invoice_receivables(self): + self.open_new_session() + + # create orders + # each order with total amount of 100. + orders = [] + # from 1st to 8th order: use the same customer (self.customer) but varies with is_invoiced and payment method. + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.cash_pm, 100)], customer=self.customer, is_invoiced=True, uid='00100-010-0001')) + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.bank_pm, 100)], customer=self.customer, is_invoiced=True, uid='00100-010-0002')) + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.cash_split_pm, 100)], customer=self.customer, is_invoiced=True, uid='00100-010-0003')) + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.bank_split_pm, 100)], customer=self.customer, is_invoiced=True, uid='00100-010-0004')) + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.cash_pm, 100)], customer=self.customer, uid='00100-010-0005')) + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.bank_pm, 100)], customer=self.customer, uid='00100-010-0006')) + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.cash_split_pm, 100)], customer=self.customer, uid='00100-010-0007')) + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.bank_split_pm, 100)], customer=self.customer, uid='00100-010-0008')) + # 9th and 10th orders for self.other_customer, both invoiced and paid by bank + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.bank_pm, 100)], customer=self.other_customer, is_invoiced=True, uid='00100-010-0009')) + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.bank_pm, 100)], customer=self.other_customer, is_invoiced=True, uid='00100-010-0010')) + # 11th order: invoiced to self.customer with bank payment method + orders.append(self.create_ui_order_data([(self.product1, 10)], payments=[(self.bank_pm, 100)], customer=self.customer, is_invoiced=True, uid='00100-010-0011')) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # close the session + self.pos_session.action_pos_session_validate() + + # self.customer's bank split payments + customer_pos_receivable_bank = self.pos_session.move_id.line_ids.filtered(lambda line: line.partner_id == self.customer and 'Split (Bank) PM' in line.name) + self.assertEqual(len(customer_pos_receivable_bank), 2, msg='there are 2 bank split payments from self.customer') + self.assertEqual(bool(customer_pos_receivable_bank.full_reconcile_id), False, msg="the pos (bank) receivable lines shouldn't be reconciled") + + # self.customer's cash split payments + customer_pos_receivable_cash = self.pos_session.move_id.line_ids.filtered(lambda line: line.partner_id == self.customer and 'Split (Cash) PM' in line.name) + self.assertEqual(len(customer_pos_receivable_cash), 2, msg='there are 2 cash split payments from self.customer') + self.assertEqual(bool(customer_pos_receivable_cash.full_reconcile_id), True, msg="cash pos (cash) receivable lines should be reconciled") + + # self.customer's invoice receivable counterpart + customer_invoice_receivable_counterpart = self.pos_session.move_id.line_ids.filtered(lambda line: line.partner_id == self.customer and 'From invoiced orders' in line.name) + self.assertEqual(len(customer_invoice_receivable_counterpart), 1, msg='there should one aggregated invoice receivable counterpart for self.customer') + self.assertEqual(bool(customer_invoice_receivable_counterpart.full_reconcile_id), True, msg='the aggregated receivable for self.customer should be reconciled') + self.assertEqual(customer_invoice_receivable_counterpart.balance, -500, msg='aggregated balance should be -500') + + # self.other_customer also made invoiced orders + # therefore, it should also have aggregated receivable counterpart in the session's account_move + other_customer_invoice_receivable_counterpart = self.pos_session.move_id.line_ids.filtered(lambda line: line.partner_id == self.other_customer and 'From invoiced orders' in line.name) + self.assertEqual(len(other_customer_invoice_receivable_counterpart), 1, msg='there should one aggregated invoice receivable counterpart for self.other_customer') + self.assertEqual(bool(other_customer_invoice_receivable_counterpart.full_reconcile_id), True, msg='the aggregated receivable for self.other_customer should be reconciled') + self.assertEqual(other_customer_invoice_receivable_counterpart.balance, -200, msg='aggregated balance should be -200') + + def test_cash_register_if_no_order(self): + # Process one order with product3 + self.open_new_session() + session = self.pos_session + orders = [] + order_data = self.create_ui_order_data([(self.product3, 1)]) + amount_paid = order_data['data']['amount_paid'] + self.env['pos.order'].create_from_ui([order_data]) + session.action_pos_session_closing_control() + + cash_register = session.cash_register_id + self.assertEqual(cash_register.balance_start, 0) + self.assertEqual(cash_register.balance_end_real, amount_paid) + + # Open/Close session without any order + self.open_new_session() + session = self.pos_session + session.action_pos_session_closing_control() + cash_register = session.cash_register_id + self.assertEqual(cash_register.balance_start, amount_paid) + self.assertEqual(cash_register.balance_end_real, amount_paid) + self.assertEqual(self.config.last_session_closing_cash, amount_paid) + + # Open/Close session with cash control and without any order + self.config.cash_control = True + self.open_new_session() + session = self.pos_session + session.set_cashbox_pos(amount_paid, False) + session.action_pos_session_closing_control() + self.env['account.bank.statement.cashbox'].create([{ + 'start_bank_stmt_ids': [], + 'end_bank_stmt_ids': [(4, session.cash_register_id.id,)], + 'cashbox_lines_ids': [(0, 0, {'number': 1, 'coin_value': amount_paid})], + 'is_a_template': False + }]) + session.action_pos_session_validate() + self.assertEqual(cash_register.balance_start, amount_paid) + self.assertEqual(cash_register.balance_end_real, amount_paid) + self.assertEqual(self.config.last_session_closing_cash, amount_paid) + + def test_start_balance_with_two_pos(self): + """ When having several POS with cash control, this tests ensures that each POS has its correct opening amount """ + + def open_and_check(pos_data): + self.config = pos_data['config'] + self.open_new_session() + session = self.pos_session + self.assertEqual(session.cash_register_id.balance_start, pos_data['amount_paid']) + session.set_cashbox_pos(pos_data['amount_paid'], False) + + self.config.cash_control = True + pos01_config = self.config + pos02_config = pos01_config.copy() + pos01_data = {'config': pos01_config, 'p_qty': 1, 'amount_paid': 0} + pos02_data = {'config': pos02_config, 'p_qty': 3, 'amount_paid': 0} + + for pos_data in [pos01_data, pos02_data]: + open_and_check(pos_data) + session = self.pos_session + + order_data = self.create_ui_order_data([(self.product3, pos_data['p_qty'])]) + pos_data['amount_paid'] += order_data['data']['amount_paid'] + self.env['pos.order'].create_from_ui([order_data]) + + session.action_pos_session_closing_control() + self.env['account.bank.statement.cashbox'].create([{ + 'start_bank_stmt_ids': [], + 'end_bank_stmt_ids': [(4, session.cash_register_id.id,)], + 'cashbox_lines_ids': [(0, 0, {'number': 1, 'coin_value': pos_data['amount_paid']})], + 'is_a_template': False + }]) + session.action_pos_session_validate() + + open_and_check(pos01_data) + open_and_check(pos02_data) diff --git a/addons/point_of_sale/tests/test_pos_multiple_receivable_accounts.py b/addons/point_of_sale/tests/test_pos_multiple_receivable_accounts.py new file mode 100644 index 00000000..5d87eb90 --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_multiple_receivable_accounts.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo + +from odoo import tools +from odoo.addons.point_of_sale.tests.common import TestPoSCommon + +@odoo.tests.tagged('post_install', '-at_install') +class TestPoSMultipleReceivableAccounts(TestPoSCommon): + """ Test for invoiced orders with customers having receivable account different from default + + Thus, for this test, there are two receivable accounts involved and are set in the + customers. + self.customer -> self.receivable_account + self.other_customer -> self.other_receivable_account + + NOTE That both receivable accounts above are different from the pos receivable account. + """ + + def setUp(self): + super(TestPoSMultipleReceivableAccounts, self).setUp() + self.config = self.basic_config + self.product1 = self.create_product( + 'Product 1', + self.categ_basic, + lst_price=10.99, + standard_price=5.0, + tax_ids=self.taxes['tax7'].ids, + ) + self.product2 = self.create_product( + 'Product 2', + self.categ_basic, + lst_price=19.99, + standard_price=10.0, + tax_ids=self.taxes['tax10'].ids, + sale_account=self.other_sale_account, + ) + self.product3 = self.create_product( + 'Product 3', + self.categ_basic, + lst_price=30.99, + standard_price=15.0, + tax_ids=self.taxes['tax_group_7_10'].ids, + ) + self.adjust_inventory([self.product1, self.product2, self.product3], [100, 50, 50]) + + def test_01_invoiced_order_from_other_customer(self): + """ + Orders + ====== + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + | order 1 | cash | no | product1 | 10 | 109.9 | 7.69 [7%] | 117.59 | + | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.9 | + | | | | product3 | 10 | 281.73 | 19.72 [7%] + 28.17 [10%] | 329.62 | + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + | order 2 | bank | no | product1 | 5 | 54.95 | 3.85 [7%] | 58.80 | + | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + | order 3 | bank | yes | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + | | | | product3 | 5 | 140.86 | 9.86 [7%] + 14.09 [10%] | 164.81 | + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale_account | -164.85 | + | sale_account | -281.73 | + | other_sale_account | -272.59 | + | tax 7% | -31.26 | + | tax 10% | -55.43 | + | pos receivable cash | 647.11 | + | pos receivable bank | 423.51 | + | other receivable | -264.76 | + +---------------------+---------+ + | Total balance | 0.00 | + +---------------------+---------+ + """ + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product1, 10), (self.product2, 10), (self.product3, 10)])) + orders.append(self.create_ui_order_data( + [(self.product1, 5), (self.product2, 5)], + payments=[(self.bank_pm, 158.75)], + )) + orders.append(self.create_ui_order_data( + [(self.product2, 5), (self.product3, 5)], + payments=[(self.bank_pm, 264.76)], + customer=self.other_customer, + is_invoiced=True, + uid='09876-098-0987', + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # check if there is one invoiced order + self.assertEqual(len(self.pos_session.order_ids.filtered(lambda order: order.state == 'invoiced')), 1, 'There should only be one invoiced order.') + + # close the session + self.pos_session.action_pos_session_validate() + + session_move = self.pos_session.move_id + # There should be no line corresponding the original receivable account + # But there should be a line for other_receivable_account because + # that is the property_account_receivable_id of the customer + # of the invoiced order. + receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + self.assertFalse(receivable_line, msg='There should be no move line for the original receivable account.') + other_receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.other_receivable_account) + self.assertAlmostEqual(other_receivable_line.balance, -264.76) + + def test_02_all_orders_invoiced_mixed_customers(self): + """ + Orders + ====== + +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ + | order 1 | cash | yes, other_customer | product1 | 10 | 109.90 | 7.69 [7%] | 117.59 | + | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.90 | + | | | | product3 | 10 | 281.73 | 19.72 [7%] + 28.17 [10%] | 329.62 | + +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ + | order 2 | bank | yes, customer | product1 | 5 | 54.95 | 3.85 [7%] | 58.80 | + | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ + | order 3 | bank | yes, other customer | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + | | | | product3 | 5 | 140.86 | 9.86 [7%] + 14.09 [10%] | 164.81 | + +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ + + Expected Result + =============== + +------------------+---------+ + | account | balance | + +------------------+---------+ + | receivable cash | 647.11 | + | receivable bank | 423.51 | + | other receivable | -911.87 | + | receivable | -158.75 | + +------------------+---------+ + | Total balance | 0.00 | + +------------------+---------+ + + """ + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data( + [(self.product1, 10), (self.product2, 10), (self.product3, 10)], + customer=self.other_customer, + is_invoiced=True, + uid='09876-098-0987', + )) + orders.append(self.create_ui_order_data( + [(self.product1, 5), (self.product2, 5)], + payments=[(self.bank_pm, 158.75)], + customer=self.customer, + is_invoiced=True, + uid='09876-098-0988', + )) + orders.append(self.create_ui_order_data( + [(self.product2, 5), (self.product3, 5)], + payments=[(self.bank_pm, 264.76)], + customer=self.other_customer, + is_invoiced=True, + uid='09876-098-0989', + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # check if there is one invoiced order + self.assertEqual(len(self.pos_session.order_ids.filtered(lambda order: order.state == 'invoiced')), 3, 'All orders should be invoiced.') + + # close the session + self.pos_session.action_pos_session_validate() + + session_move = self.pos_session.move_id + + receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + self.assertAlmostEqual(receivable_line.balance, -158.75) + other_receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.other_receivable_account) + self.assertAlmostEqual(other_receivable_line.balance, -911.87) + receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertAlmostEqual(receivable_line_bank.balance, 423.51) + receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 647.11) diff --git a/addons/point_of_sale/tests/test_pos_multiple_sale_accounts.py b/addons/point_of_sale/tests/test_pos_multiple_sale_accounts.py new file mode 100644 index 00000000..f5df1833 --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_multiple_sale_accounts.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo + +from odoo import tools +from odoo.addons.point_of_sale.tests.common import TestPoSCommon + +@odoo.tests.tagged('post_install', '-at_install') +class TestPoSMultipleSaleAccounts(TestPoSCommon): + """ Test to orders containing products with different sale accounts + + keywords/phrases: Different Income Accounts + + In this test, two sale (income) accounts are involved: + self.sale_account -> default for products because it is in the category + self.other_sale_account -> manually set to self.product2 + """ + + def setUp(self): + super(TestPoSMultipleSaleAccounts, self).setUp() + + self.config = self.basic_config + self.product1 = self.create_product( + 'Product 1', + self.categ_basic, + lst_price=10.99, + standard_price=5.0, + tax_ids=self.taxes['tax7'].ids, + ) + self.product2 = self.create_product( + 'Product 2', + self.categ_basic, + lst_price=19.99, + standard_price=10.0, + tax_ids=self.taxes['tax10'].ids, + sale_account=self.other_sale_account, + ) + self.product3 = self.create_product( + 'Product 3', + self.categ_basic, + lst_price=30.99, + standard_price=15.0, + tax_ids=self.taxes['tax_group_7_10'].ids, + ) + self.adjust_inventory([self.product1, self.product2, self.product3], [100, 50, 50]) + + def test_01_check_product_properties(self): + self.assertEqual(self.product2.property_account_income_id, self.other_sale_account, 'Income account for the product2 should be the other sale account.') + self.assertFalse(self.product1.property_account_income_id, msg='Income account for product1 should not be set.') + self.assertFalse(self.product3.property_account_income_id, msg='Income account for product3 should not be set.') + self.assertEqual(self.product1.categ_id.property_account_income_categ_id, self.sale_account) + self.assertEqual(self.product3.categ_id.property_account_income_categ_id, self.sale_account) + + def test_02_orders_without_invoice(self): + """ orders without invoice + + Orders + ====== + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + | order 1 | cash | no | product1 | 10 | 109.9 | 7.69 [7%] | 117.59 | + | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.9 | + | | | | product3 | 10 | 281.73 | 19.72 [7%] + 28.17 [10%] | 329.62 | + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + | order 2 | cash | no | product1 | 5 | 54.95 | 3.85 [7%] | 58.80 | + | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + | order 3 | bank | no | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + | | | | product3 | 5 | 140.86 | 9.86 [7%] + 14.09 [10%] | 164.81 | + +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale_account | -164.85 | (for the 7% base amount) + | sale_account | -422.59 | (for the 7+10% base amount) + | other_sale_account | -363.45 | + | tax 7% | -41.12 | + | tax 10% | -78.61 | + | pos receivable bank | 264.76 | + | pos receivable cash | 805.86 | + +---------------------+---------+ + | Total balance | 0.00 | + +---------------------+---------+ + """ + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product1, 10), (self.product2, 10), (self.product3, 10)])) + orders.append(self.create_ui_order_data([(self.product1, 5), (self.product2, 5)])) + orders.append(self.create_ui_order_data([(self.product2, 5), (self.product3, 5)], payments=[(self.bank_pm, 264.76)])) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_move = self.pos_session.move_id + + sale_account_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + for balance, amount in zip(sorted(sale_account_lines.mapped('balance')), sorted([-164.85, -422.59])): + self.assertAlmostEqual(balance, amount) + + other_sale_account_line = session_move.line_ids.filtered(lambda line: line.account_id == self.other_sale_account) + self.assertAlmostEqual(other_sale_account_line.balance, -363.45) + + receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertAlmostEqual(receivable_line_bank.balance, 264.76) + + receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 805.86) + + manually_calculated_taxes = (-41.12, -78.61) + tax_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + self.assertAlmostEqual(sum(manually_calculated_taxes), sum(tax_lines.mapped('balance'))) + for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): + self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined.') + + self.assertTrue(receivable_line_cash.full_reconcile_id) + + def test_03_orders_with_invoice(self): + """ orders with invoice + + Orders + ====== + +---------+----------+---------------+----------+-----+---------+--------------------------+--------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +---------+----------+---------------+----------+-----+---------+--------------------------+--------+ + | order 1 | cash | no | product1 | 10 | 109.9 | 7.69 [7%] | 117.59 | + | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.9 | + | | | | product3 | 10 | 281.73 | 19.72 [7%] + 28.17 [10%] | 329.62 | + +---------+----------+---------------+----------+-----+---------+--------------------------+--------+ + | order 2 | bank | no | product1 | 5 | 54.95 | 3.85 [7%] | 58.80 | + | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + +---------+----------+---------------+----------+-----+---------+--------------------------+--------+ + | order 3 | bank | yes, customer | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + | | | | product3 | 5 | 140.86 | 9.86 [7%] + 14.09 [10%] | 164.81 | + +---------+----------+---------------+----------+-----+---------+--------------------------+--------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale_account | -164.85 | (for the 7% base amount) + | sale_account | -281.73 | (for the 7+10% base amount) + | other_sale_account | -272.59 | + | tax 7% | -31.26 | + | tax 10% | -55.43 | + | pos receivable cash | 647.11 | + | pos receivable bank | 423.51 | + | receivable | -264.76 | + +---------------------+---------+ + | Total balance | 0.00 | + +---------------------+---------+ + """ + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product1, 10), (self.product2, 10), (self.product3, 10)])) + orders.append(self.create_ui_order_data( + [(self.product1, 5), (self.product2, 5)], + payments=[(self.bank_pm, 158.75)], + )) + orders.append(self.create_ui_order_data( + [(self.product2, 5), (self.product3, 5)], + payments=[(self.bank_pm, 264.76)], + customer=self.customer, + is_invoiced=True, + uid='09876-098-0987', + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # check if there is one invoiced order + self.assertEqual(len(self.pos_session.order_ids.filtered(lambda order: order.state == 'invoiced')), 1, 'There should only be one invoiced order.') + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_move = self.pos_session.move_id + + sale_account_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + for balance, amount in zip(sorted(sale_account_lines.mapped('balance')), sorted([-164.85, -281.73])): + self.assertAlmostEqual(balance, amount) + + other_sale_account_line = session_move.line_ids.filtered(lambda line: line.account_id == self.other_sale_account) + self.assertAlmostEqual(other_sale_account_line.balance, -272.59) + + pos_receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertAlmostEqual(pos_receivable_line_bank.balance, 423.51) + + pos_receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertAlmostEqual(pos_receivable_line_cash.balance, 647.11) + + manually_calculated_taxes = (-31.26, -55.43) + tax_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + self.assertAlmostEqual(sum(manually_calculated_taxes), sum(tax_lines.mapped('balance'))) + for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): + self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined.') + + receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + self.assertAlmostEqual(receivable_line.balance, -264.76) + + self.assertTrue(pos_receivable_line_cash.full_reconcile_id) + self.assertTrue(receivable_line.full_reconcile_id) 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 new file mode 100644 index 00000000..496a26ad --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_other_currency_config.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo + +from odoo import tools +from odoo.tests.common import Form +from odoo.addons.point_of_sale.tests.common import TestPoSCommon + +@odoo.tests.tagged('post_install', '-at_install') +class TestPoSOtherCurrencyConfig(TestPoSCommon): + """ Test PoS with basic configuration + """ + + def setUp(self): + super(TestPoSOtherCurrencyConfig, self).setUp() + + self.config = self.other_currency_config + 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.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, + 'fixed_price': 12.99, + }) + 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) + # Except for product2 where the price is specified in the pricelist. + + 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 + + Orders + ====== + +---------+----------+-----------+----------+-----+-------+ + | order | payments | invoiced? | product | qty | total | + +---------+----------+-----------+----------+-----+-------+ + | order 1 | cash | no | product1 | 10 | 50 | + | | | | product2 | 10 | 129.9 | + | | | | product3 | 10 | 150 | + +---------+----------+-----------+----------+-----+-------+ + | order 2 | cash | no | product1 | 5 | 25 | + | | | | product2 | 5 | 64.95 | + +---------+----------+-----------+----------+-----+-------+ + | order 3 | bank | no | product2 | 5 | 64.95 | + | | | | product3 | 5 | 75 | + +---------+----------+-----------+----------+-----+-------+ + + Expected Result + =============== + +---------------------+---------+-----------------+ + | account | balance | amount_currency | + +---------------------+---------+-----------------+ + | sale_account | -1119.6 | -559.80 | + | pos receivable bank | 279.9 | 139.95 | + | pos receivable cash | 839.7 | 419.85 | + +---------------------+---------+-----------------+ + | Total balance | 0.0 | 0.00 | + +---------------------+---------+-----------------+ + """ + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product1, 10), (self.product2, 10), (self.product3, 10)])) + orders.append(self.create_ui_order_data([(self.product1, 5), (self.product2, 5)])) + orders.append(self.create_ui_order_data([(self.product2, 5), (self.product3, 5)], payments=[(self.bank_pm, 139.95)])) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # close the session + self.pos_session.action_pos_session_validate() + session_move = self.pos_session.move_id + + sale_account_line = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sale_account_line.balance, -1119.6) + self.assertAlmostEqual(sale_account_line.amount_currency, -559.80) + + pos_receivable_line_bank = session_move.line_ids.filtered(lambda line: line.account_id == self.pos_receivable_account and self.bank_pm.name in line.name) + self.assertAlmostEqual(pos_receivable_line_bank.balance, 279.9) + self.assertAlmostEqual(pos_receivable_line_bank.amount_currency, 139.95) + + pos_receivable_line_cash = session_move.line_ids.filtered(lambda line: line.account_id == self.pos_receivable_account and self.cash_pm.name in line.name) + self.assertAlmostEqual(pos_receivable_line_cash.balance, 839.7) + self.assertAlmostEqual(pos_receivable_line_cash.amount_currency, 419.85) + + def test_03_orders_with_invoice(self): + """ orders with invoice + + Orders + ====== + +---------+----------+---------------+----------+-----+-------+ + | order | payments | invoiced? | product | qty | total | + +---------+----------+---------------+----------+-----+-------+ + | order 1 | cash | no | product1 | 10 | 50 | + | | | | product2 | 10 | 129.9 | + | | | | product3 | 10 | 150 | + +---------+----------+---------------+----------+-----+-------+ + | order 2 | cash | yes, customer | product1 | 5 | 25 | + | | | | product2 | 5 | 64.95 | + +---------+----------+---------------+----------+-----+-------+ + | order 3 | bank | yes, customer | product2 | 5 | 64.95 | + | | | | product3 | 5 | 75 | + +---------+----------+---------------+----------+-----+-------+ + + Expected Result + =============== + +---------------------+---------+-----------------+ + | account | balance | amount_currency | + +---------------------+---------+-----------------+ + | sale_account | -659.8 | -329.90 | + | pos receivable bank | 279.9 | 139.95 | + | pos receivable cash | 839.7 | 419.85 | + | invoice receivable | -459.8 | -229.90 | + +---------------------+---------+-----------------+ + | Total balance | 0.0 | 0.00 | + +---------------------+---------+-----------------+ + """ + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product1, 10), (self.product2, 10), (self.product3, 10)])) + orders.append(self.create_ui_order_data( + [(self.product1, 5), (self.product2, 5)], + customer=self.customer, + is_invoiced=True, + )) + orders.append(self.create_ui_order_data( + [(self.product2, 5), (self.product3, 5)], + payments=[(self.bank_pm, 139.95)], + customer=self.customer, + is_invoiced=True, + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # close the session + self.pos_session.action_pos_session_validate() + session_move = self.pos_session.move_id + + sale_account_line = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sale_account_line.balance, -659.8) + self.assertAlmostEqual(sale_account_line.amount_currency, -329.9) + + pos_receivable_line_bank = session_move.line_ids.filtered(lambda line: line.account_id == self.pos_receivable_account and self.bank_pm.name in line.name) + self.assertAlmostEqual(pos_receivable_line_bank.balance, 279.9) + self.assertAlmostEqual(pos_receivable_line_bank.amount_currency, 139.95) + + pos_receivable_line_cash = session_move.line_ids.filtered(lambda line: line.account_id == self.pos_receivable_account and self.cash_pm.name in line.name) + self.assertAlmostEqual(pos_receivable_line_cash.balance, 839.7) + self.assertAlmostEqual(pos_receivable_line_cash.amount_currency, 419.85) + + 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 | 2375.99 | + | output_account | -2375.99 | -2375.99 | + +---------------------+------------+-----------------+ + | 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.assertAlmostEqual(expense_line.amount_currency, 2375.99) + + output_line = session_account_move.line_ids.filtered(lambda line: line.account_id == self.output_account) + self.assertAlmostEqual(output_line.balance, -2375.99) + self.assertAlmostEqual(output_line.amount_currency, -2375.99) + + 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.43) + self.assertAlmostEqual(receivable_line_cash.amount_currency, 26.215) + + tax_line = session_account_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + self.assertAlmostEqual(tax_line.balance, -3.43) + self.assertAlmostEqual(tax_line.amount_currency, -1.715) + self.assertAlmostEqual(tax_line.tax_base_amount, 49, msg="Value should be in company's currency.") diff --git a/addons/point_of_sale/tests/test_pos_products_with_tax.py b/addons/point_of_sale/tests/test_pos_products_with_tax.py new file mode 100644 index 00000000..b0ef8ac8 --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_products_with_tax.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import tools + +import odoo +from odoo.addons.point_of_sale.tests.common import TestPoSCommon + +@odoo.tests.tagged('post_install', '-at_install') +class TestPoSProductsWithTax(TestPoSCommon): + """ Test normal configuration PoS selling products with tax + """ + + def setUp(self): + super(TestPoSProductsWithTax, self).setUp() + + self.config = self.basic_config + self.product1 = self.create_product( + 'Product 1', + self.categ_basic, + 10.0, + 5.0, + tax_ids=self.taxes['tax7'].ids, + ) + self.product2 = self.create_product( + 'Product 2', + self.categ_basic, + 20.0, + 10.0, + tax_ids=self.taxes['tax10'].ids, + ) + self.product3 = self.create_product( + 'Product 3', + self.categ_basic, + 30.0, + 15.0, + tax_ids=self.taxes['tax_group_7_10'].ids, + ) + self.adjust_inventory([self.product1, self.product2, self.product3], [100, 50, 50]) + + def test_orders_no_invoiced(self): + """ Test for orders without invoice + + Orders + ====== + +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ + | order 1 | cash | no | product1 | 10 | 100 | 7 | 107 | + | | | | product2 | 5 | 90.91 | 9.09 | 100 | + +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ + | order 2 | cash | no | product2 | 7 | 127.27 | 12.73 | 140 | + | | | | product3 | 4 | 109.09 | 10.91[10%] + 7.64[7%] | 127.64 | + +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ + | order 3 | bank | no | product1 | 1 | 10 | 0.7 | 10.7 | + | | | | product2 | 3 | 54.55 | 5.45 | 60 | + | | | | product3 | 5 | 136.36 | 13.64[10%] + 9.55[7%] | 159.55 | + +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ + + Calculated taxes + ================ + total tax 7% only + group tax (10+7%) + (7 + 0.7) + (7.64 + 9.55) = 7.7 + 17.19 = 24.89 + total tax 10% only + group tax (10+7%) + (9.09 + 12.73 + 5.45) + (10.91 + 13.64) = 27.27 + 24.55 = 51.82 + + Thus, manually_calculated_taxes = (-24,89, -51.82) + """ + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product1, 10), (self.product2, 5)])) + orders.append(self.create_ui_order_data([(self.product2, 7), (self.product3, 4)])) + orders.append(self.create_ui_order_data( + [(self.product1, 1), (self.product3, 5), (self.product2, 3)], + payments=[(self.bank_pm, 230.25)] + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, 'Total order amount should be equal to the total payment amount.') + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_move = self.pos_session.move_id + + sales_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sum(sales_lines.mapped('balance')), -628.18, msg='Sales line balance should be equal to untaxed orders amount.') + + receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertAlmostEqual(receivable_line_bank.balance, 230.25, msg='Bank receivable should be equal to the total bank payments.') + + receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 474.64, msg='Cash receivable should be equal to the total cash payments.') + + tax_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + + manually_calculated_taxes = (-24.89, -51.82) + self.assertAlmostEqual(sum(manually_calculated_taxes), sum(tax_lines.mapped('balance'))) + for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): + self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined.') + + base_amounts = (355.45, 518.18) + self.assertAlmostEqual(sum(base_amounts), sum(tax_lines.mapped('tax_base_amount'))) + + self.assertTrue(receivable_line_cash.full_reconcile_id, 'Cash receivable line should be fully-reconciled.') + + def test_orders_with_invoiced(self): + """ Test for orders: one with invoice + + Orders + ====== + +---------+----------+---------------+----------+-----+---------+---------------+--------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +---------+----------+---------------+----------+-----+---------+---------------+--------+ + | order 1 | cash | no | product1 | 6 | 60 | 4.2 | 64.2 | + | | | | product2 | 3 | 54.55 | 5.45 | 60 | + | | | | product3 | 1 | 27.27 | 2.73 + 1.91 | 31.91 | + +---------+----------+---------------+----------+-----+---------+---------------+--------+ + | order 2 | bank | no | product1 | 1 | 10 | 0.7 | 10.7 | + | | | | product2 | 20 | 363.64 | 36.36 | 400 | + +---------+----------+---------------+----------+-----+---------+---------------+--------+ + | order 3 | bank | yes, customer | product1 | 10 | 100 | 7 | 107 | + | | | | product3 | 10 | 272.73 | 27.27 + 19.09 | 319.09 | + +---------+----------+---------------+----------+-----+---------+---------------+--------+ + + Calculated taxes + ================ + total tax 7% only + 4.2 + 0.7 => 4.9 + 1.91 = 6.81 + total tax 10% only + 5.45 + 36.36 => 41.81 + 2.73 = 44.54 + + Thus, manually_calculated_taxes = (-6.81, -44.54) + """ + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data( + [(self.product3, 1), (self.product1, 6), (self.product2, 3)], + payments=[(self.cash_pm, 156.11)], + )) + orders.append(self.create_ui_order_data( + [(self.product2, 20), (self.product1, 1)], + payments=[(self.bank_pm, 410.7)], + )) + orders.append(self.create_ui_order_data( + [(self.product1, 10), (self.product3, 10)], + payments=[(self.bank_pm, 426.09)], + customer=self.customer, + is_invoiced=True, + uid='09876-098-0987', + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # check account move in the invoiced order + invoiced_order = self.pos_session.order_ids.filtered(lambda order: '09876-098-0987' in order.pos_reference) + self.assertEqual(1, len(invoiced_order), 'Only one order is invoiced in this test.') + invoice = invoiced_order.account_move + self.assertAlmostEqual(invoice.amount_total, 426.09) + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_move = self.pos_session.move_id + + # check sales line + # should not include tax amounts + sales_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sum(sales_lines.mapped('balance')), -515.46) + + # check receivable line + # should be equivalent to receivable in the invoice + # should also be fully-reconciled + receivable_line = session_move.line_ids.filtered(lambda line: line.account_id in self.receivable_account + self.env['account.account'].search([('name', '=', 'Account Receivable')]) and line.name == 'From invoiced orders') + self.assertAlmostEqual(receivable_line.balance, -426.09) + self.assertTrue(receivable_line.full_reconcile_id, msg='Receivable line for invoices should be fully reconciled.') + + pos_receivable_line_bank = session_move.line_ids.filtered( + lambda line: self.bank_pm.name in line.name and line.account_id == self.bank_pm.receivable_account_id + ) + self.assertAlmostEqual(pos_receivable_line_bank.balance, 836.79) + + pos_receivable_line_cash = session_move.line_ids.filtered( + lambda line: self.cash_pm.name in line.name and line.account_id == self.bank_pm.receivable_account_id + ) + self.assertAlmostEqual(pos_receivable_line_cash.balance, 156.11) + self.assertTrue(pos_receivable_line_cash.full_reconcile_id) + + receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + self.assertAlmostEqual(receivable_line.balance, -invoice.amount_total) + + tax_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + + manually_calculated_taxes = (-6.81, -44.54) + self.assertAlmostEqual(sum(manually_calculated_taxes), sum(tax_lines.mapped('balance'))) + for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): + self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined.') + + base_amounts = (97.27, 445.46) # computation does not include invoiced order. + self.assertAlmostEqual(sum(base_amounts), sum(tax_lines.mapped('tax_base_amount'))) + + def test_return_order(self): + """ Test return order + + Order (invoiced) + ====== + +----------+----------+---------------+----------+-----+---------+-------------+-------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +----------+----------+---------------+----------+-----+---------+-------------+-------+ + | order 1 | cash | yes, customer | product1 | 3 | 30 | 2.1 | 32.1 | + | | | | product2 | 2 | 36.36 | 3.64 | 40 | + | | | | product3 | 1 | 27.27 | 2.73 + 1.91 | 31.91 | + +----------+----------+---------------+----------+-----+---------+-------------+-------+ + + The order is invoiced so the tax of the invoiced order is in the account_move of the order. + However, the return order is not invoiced, thus, the journal items are in the session_move, + which will contain the tax lines of the returned products. + + manually_calculated_taxes = (4.01, 6.37) + """ + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data( + [(self.product1, 3), (self.product2, 2), (self.product3, 1)], + payments=[(self.cash_pm, 104.01)], + customer=self.customer, + is_invoiced=True, + uid='12345-123-1234', + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(1, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # return order + order_to_return = self.pos_session.order_ids.filtered(lambda order: '12345-123-1234' in order.pos_reference) + order_to_return.refund() + + refund_order = self.pos_session.order_ids.filtered(lambda order: order.state == 'draft') + context_make_payment = {"active_ids": [refund_order.id], "active_id": refund_order.id} + make_payment = self.env['pos.make.payment'].with_context(context_make_payment).create({ + 'payment_method_id': self.cash_pm.id, + 'amount': -104.01, + }) + make_payment.check() + self.assertEqual(refund_order.state, 'paid', 'Payment is registered, order should be paid.') + self.assertAlmostEqual(refund_order.amount_paid, -104.01, msg='Amount paid for return order should be negative.') + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_move = self.pos_session.move_id + + # instead of credit, the sales line should be debit + sales_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sum(sales_lines.mapped('balance')), 93.63) + + receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertFalse(receivable_line_bank, msg='There should be no bank receivable line because no bank payment made.') + + receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertFalse(receivable_line_cash, msg='There should be no cash receivable line because it is combined with the original cash payment.') + + manually_calculated_taxes = (4.01, 6.37) # should be positive since it is return order + tax_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + self.assertAlmostEqual(sum(manually_calculated_taxes), sum(tax_lines.mapped('balance'))) + for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): + self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined and should be debit.') diff --git a/addons/point_of_sale/tests/test_pos_setup.py b/addons/point_of_sale/tests/test_pos_setup.py new file mode 100644 index 00000000..22fa68b3 --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_setup.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import tools +import odoo +from odoo.addons.point_of_sale.tests.common import TestPoSCommon + +@odoo.tests.tagged('post_install', '-at_install') +class TestPoSSetup(TestPoSCommon): + """ This group of tests is for sanity check in setting up global records which will be used + in each testing. + + If a test fails here, then it means there are inconsistencies in what we expect in the setup. + """ + def setUp(self): + super(TestPoSSetup, self).setUp() + + self.config = self.basic_config + self.products = [ + self.create_product('Product 1', self.categ_basic, lst_price=10.0, standard_price=5), + self.create_product('Product 2', self.categ_basic, lst_price=20.0, standard_price=10), + self.create_product('Product 3', self.categ_basic, lst_price=30.0, standard_price=15), + ] + + def test_basic_config_values(self): + + config = self.basic_config + self.assertEqual(config.currency_id, self.company_currency) + self.assertEqual(config.pricelist_id.currency_id, self.company_currency) + + def test_other_currency_config_values(self): + config = self.other_currency_config + self.assertEqual(config.currency_id, self.other_currency) + self.assertEqual(config.pricelist_id.currency_id, self.other_currency) + + def test_product_categories(self): + # check basic product category + # it is expected to have standard and manual_periodic valuation + self.assertEqual(self.categ_basic.property_cost_method, 'standard') + self.assertEqual(self.categ_basic.property_valuation, 'manual_periodic') + # check anglo saxon product category + # this product categ is expected to have fifo and real_time valuation + self.assertEqual(self.categ_anglo.property_cost_method, 'fifo') + self.assertEqual(self.categ_anglo.property_valuation, 'real_time') + + def test_product_price(self): + def get_price(pricelist, product): + return pricelist.get_product_price(product, 1, self.customer) + + + # check usd pricelist + pricelist = self.basic_config.pricelist_id + for product in self.products: + self.assertAlmostEqual(get_price(pricelist, product), product.lst_price) + + # check eur pricelist + # exchange rate to the other currency is set to 0.5, thus, lst_price + # is expected to have half its original value. + pricelist = self.other_currency_config.pricelist_id + for product in self.products: + self.assertAlmostEqual(get_price(pricelist, product), product.lst_price * 0.5) + + def test_taxes(self): + tax7 = self.taxes['tax7'] + self.assertEqual(tax7.name, 'Tax 7%') + self.assertAlmostEqual(tax7.amount, 7) + self.assertEqual(tax7.invoice_repartition_line_ids.mapped('account_id').id, self.tax_received_account.id) + tax10 = self.taxes['tax10'] + self.assertEqual(tax10.name, 'Tax 10%') + self.assertAlmostEqual(tax10.amount, 10) + self.assertEqual(tax10.price_include, True) + self.assertEqual(tax10.invoice_repartition_line_ids.mapped('account_id').id, self.tax_received_account.id) + tax_group_7_10 = self.taxes['tax_group_7_10'] + self.assertEqual(tax_group_7_10.name, 'Tax 7+10%') + self.assertEqual(tax_group_7_10.amount_type, 'group') + self.assertEqual(sorted(tax_group_7_10.children_tax_ids.ids), sorted((tax7 | tax10).ids)) diff --git a/addons/point_of_sale/tests/test_pos_stock_account.py b/addons/point_of_sale/tests/test_pos_stock_account.py new file mode 100644 index 00000000..287b4430 --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_stock_account.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import tools +import odoo +from odoo.addons.point_of_sale.tests.common import TestPoSCommon + +@odoo.tests.tagged('post_install', '-at_install') +class TestPoSStock(TestPoSCommon): + """ Tests for anglo saxon accounting scenario. + """ + def setUp(self): + super(TestPoSStock, self).setUp() + + self.config = self.basic_config + self.product1 = self.create_product('Product 1', self.categ_anglo, 10.0, 5.0) + self.product2 = self.create_product('Product 2', self.categ_anglo, 20.0, 10.0) + self.product3 = self.create_product('Product 3', self.categ_basic, 30.0, 15.0) + # start inventory with 10 items for each product + self.adjust_inventory([self.product1, self.product2, self.product3], [10, 10, 10]) + + # change cost(standard_price) of anglo products + # then set inventory from 10 -> 15 + self.product1.write({'standard_price': 6.0}) + self.product2.write({'standard_price': 6.0}) + self.adjust_inventory([self.product1, self.product2, self.product3], [15, 15, 15]) + + # change cost(standard_price) of anglo products + # then set inventory from 15 -> 25 + self.product1.write({'standard_price': 13.0}) + self.product2.write({'standard_price': 13.0}) + self.adjust_inventory([self.product1, self.product2, self.product3], [25, 25, 25]) + + self.output_account = self.categ_anglo.property_stock_account_output_categ_id + self.expense_account = self.categ_anglo.property_account_expense_categ_id + self.valuation_account = self.categ_anglo.property_stock_valuation_account_id + + def test_01_orders_no_invoiced(self): + """ + + Orders + ====== + +---------+----------+-----+-------------+------------+ + | order | product | qty | total price | total cost | + +---------+----------+-----+-------------+------------+ + | order 1 | product1 | 10 | 100.0 | 50.0 | -> 10 items at cost of 5.0 is consumed, remains 5 items at 6.0 and 10 items at 13.0 + | | product2 | 10 | 200.0 | 100.0 | -> 10 items at cost of 10.0 is consumed, remains 5 items at 6.0 and 10 items at 13.0 + +---------+----------+-----+-------------+------------+ + | order 2 | product2 | 7 | 140.0 | 56.0 | -> 5 items at cost of 6.0 and 2 items at cost of 13.0, remains 8 items at cost of 13.0 + | | product3 | 7 | 210.0 | 0.0 | + +---------+----------+-----+-------------+------------+ + | order 3 | product1 | 6 | 60.0 | 43.0 | -> 5 items at cost of 6.0 and 1 item at cost of 13.0, remains 9 items at cost of 13.0 + | | product2 | 6 | 120.0 | 78.0 | -> 6 items at cost of 13.0, remains 2 items at cost of 13.0 + | | product3 | 6 | 180.0 | 0.0 | + +---------+----------+-----+-------------+------------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale_account | -1010.0 | + | pos_receivable-cash | 1010.0 | + | expense_account | 327.0 | + | output_account | -327.0 | + +---------------------+---------+ + | Total balance | 0.00 | + +---------------------+---------+ + """ + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product1, 10), (self.product2, 10)])) + orders.append(self.create_ui_order_data([(self.product2, 7), (self.product3, 7)])) + orders.append(self.create_ui_order_data([(self.product1, 6), (self.product2, 6), (self.product3, 6)])) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + self.assertAlmostEqual(orders_total, 1010.0, msg='The orders\'s total amount should equal the computed.') + + # check product qty_available after syncing the order + self.assertEqual(self.product1.qty_available, 9) + self.assertEqual(self.product2.qty_available, 2) + self.assertEqual(self.product3.qty_available, 12) + + # picking and stock moves should be in done state + for order in self.pos_session.order_ids: + self.assertEqual(order.picking_ids[0].state, 'done', 'Picking should be in done state.') + self.assertTrue(all(state == 'done' for state in order.picking_ids[0].move_lines.mapped('state')), 'Move Lines should be in done state.' ) + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + account_move = self.pos_session.move_id + + sales_line = account_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sales_line.balance, -orders_total, msg='Sales line balance should be equal to total orders amount.') + + receivable_line_cash = account_move.line_ids.filtered(lambda line: line.account_id in self.pos_receivable_account + self.env['account.account'].search([('name', '=', 'Account Receivable (PoS)')]) and self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 1010.0, msg='Cash receivable should be equal to the total cash payments.') + + expense_line = account_move.line_ids.filtered(lambda line: line.account_id == self.expense_account) + self.assertAlmostEqual(expense_line.balance, 327.0) + + output_line = account_move.line_ids.filtered(lambda line: line.account_id == self.output_account) + self.assertAlmostEqual(output_line.balance, -327.0) + + 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_02_orders_with_invoice(self): + """ + + Orders + ====== + Same with test_01 but order 3 is invoiced. + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale_account | -650.0 | + | pos_receivable-cash | 1010.0 | + | receivable | -360.0 | + | expense_account | 206.0 | + | output_account | -206.0 | + +---------------------+---------+ + | Total balance | 0.00 | + +---------------------+---------+ + """ + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product1, 10), (self.product2, 10)])) + orders.append(self.create_ui_order_data([(self.product2, 7), (self.product3, 7)])) + invoiced_uid = self.create_random_uid() + orders.append(self.create_ui_order_data( + [(self.product1, 6), (self.product2, 6), (self.product3, 6)], + is_invoiced=True, + customer=self.customer, + uid=invoiced_uid, + )) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + self.assertAlmostEqual(orders_total, 1010.0, msg='The orders\'s total amount should equal the computed.') + + # check product qty_available after syncing the order + self.assertEqual(self.product1.qty_available, 9) + self.assertEqual(self.product2.qty_available, 2) + self.assertEqual(self.product3.qty_available, 12) + + # picking and stock moves should be in done state + for order in self.pos_session.order_ids: + self.assertEqual(order.picking_ids[0].state, 'done', 'Picking should be in done state.') + self.assertTrue(all(state == 'done' for state in order.picking_ids[0].move_lines.mapped('state')), 'Move Lines should be in done state.' ) + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + account_move = self.pos_session.move_id + + sales_line = account_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertAlmostEqual(sales_line.balance, -650.0) + + receivable_line = account_move.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + self.assertAlmostEqual(receivable_line.balance, -360.0, msg='Receivable line balance should equal the negative of total amount of invoiced orders.') + + receivable_line_cash = account_move.line_ids.filtered(lambda line: line.account_id in self.pos_receivable_account + self.env['account.account'].search([('name', '=', 'Account Receivable (PoS)')]) and self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 1010.0, msg='Cash receivable should be equal to the total cash payments.') + + expense_line = account_move.line_ids.filtered(lambda line: line.account_id == self.expense_account) + self.assertAlmostEqual(expense_line.balance, 206.0) + + output_line = account_move.line_ids.filtered(lambda line: line.account_id == self.output_account) + self.assertAlmostEqual(output_line.balance, -206.0) + + # check order journal entry + invoiced_order = self.pos_session.order_ids.filtered(lambda order: invoiced_uid in order.pos_reference) + invoiced_output_account_lines = invoiced_order.account_move.line_ids.filtered(lambda line: line.account_id == self.output_account) + self.assertAlmostEqual(sum(invoiced_output_account_lines.mapped('balance')), -121.0) + + # The stock output account move lines of the invoiced order should be properly reconciled + for move_line in invoiced_order.account_move.line_ids.filtered(lambda line: line.account_id == self.output_account): + self.assertTrue(move_line.full_reconcile_id) + + 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_03_order_product_w_owner(self): + """ + Test order via POS a product having stock owner. + """ + + group_owner = self.env.ref('stock.group_tracking_owner') + self.env.user.write({'groups_id': [(4, group_owner.id)]}) + self.product4 = self.create_product('Product 3', self.categ_basic, 30.0, 15.0) + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory adjustment' + }) + self.env['stock.inventory.line'].create({ + 'product_id': self.product4.id, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + 'inventory_id': inventory.id, + 'product_qty': 10, + 'partner_id': self.partner_a.id, + 'location_id': self.stock_location_components.id, + }) + inventory._action_start() + inventory.action_validate() + + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data([(self.product4, 1)])) + + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(1, self.pos_session.order_count) + + # check product qty_available after syncing the order + self.assertEqual(self.product4.qty_available, 9) + + # picking and stock moves should be in done state + for order in self.pos_session.order_ids: + self.assertEqual(order.picking_ids[0].state, 'done', 'Picking should be in done state.') + self.assertTrue(all(state == 'done' for state in order.picking_ids[0].move_lines.mapped('state')), 'Move Lines should be in done state.' ) + self.assertTrue(self.partner_a == order.picking_ids[0].move_lines[0].move_line_ids[0].owner_id, 'Move Lines Owner should be taken into account.' ) + + # close the session + self.pos_session.action_pos_session_validate() diff --git a/addons/point_of_sale/tests/test_pos_with_fiscal_position.py b/addons/point_of_sale/tests/test_pos_with_fiscal_position.py new file mode 100644 index 00000000..8239e877 --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_with_fiscal_position.py @@ -0,0 +1,390 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import tools +import odoo +from odoo.addons.point_of_sale.tests.common import TestPoSCommon + +@odoo.tests.tagged('post_install', '-at_install') +class TestPoSWithFiscalPosition(TestPoSCommon): + """ Tests to pos orders with fiscal position. + + keywords/phrases: fiscal position + """ + + @classmethod + def setUpClass(cls): + super(TestPoSWithFiscalPosition, cls).setUpClass() + + cls.config = cls.basic_config + + cls.new_tax_17 = cls.env['account.tax'].create({'name': 'New Tax 17%', 'amount': 17}) + cls.new_tax_17.invoice_repartition_line_ids.write({'account_id': cls.tax_received_account.id}) + + cls.fpos = cls._create_fiscal_position() + cls.fpos_no_tax_dest = cls._create_fiscal_position_no_tax_dest() + + cls.product1 = cls.create_product( + 'Product 1', + cls.categ_basic, + lst_price=10.99, + standard_price=5.0, + tax_ids=cls.taxes['tax7'].ids, + ) + cls.product2 = cls.create_product( + 'Product 2', + cls.categ_basic, + lst_price=19.99, + standard_price=10.0, + tax_ids=cls.taxes['tax10'].ids, + ) + cls.product3 = cls.create_product( + 'Product 3', + cls.categ_basic, + lst_price=30.99, + standard_price=15.0, + tax_ids=cls.taxes['tax7'].ids, + ) + cls.adjust_inventory([cls.product1, cls.product2, cls.product3], [100, 50, 50]) + + @classmethod + def _create_fiscal_position(cls): + fpos = cls.env['account.fiscal.position'].create({'name': 'Test Fiscal Position'}) + + account_fpos = cls.env['account.fiscal.position.account'].create({ + 'position_id': fpos.id, + 'account_src_id': cls.sale_account.id, + 'account_dest_id': cls.other_sale_account.id, + }) + tax_fpos = cls.env['account.fiscal.position.tax'].create({ + 'position_id': fpos.id, + 'tax_src_id': cls.taxes['tax7'].id, + 'tax_dest_id': cls.new_tax_17.id, + }) + fpos.write({ + 'account_ids': [(6, 0, account_fpos.ids)], + 'tax_ids': [(6, 0, tax_fpos.ids)], + }) + return fpos + + @classmethod + def _create_fiscal_position_no_tax_dest(cls): + fpos_no_tax_dest = cls.env['account.fiscal.position'].create({'name': 'Test Fiscal Position'}) + account_fpos = cls.env['account.fiscal.position.account'].create({ + 'position_id': fpos_no_tax_dest.id, + 'account_src_id': cls.sale_account.id, + 'account_dest_id': cls.other_sale_account.id, + }) + tax_fpos = cls.env['account.fiscal.position.tax'].create({ + 'position_id': fpos_no_tax_dest.id, + 'tax_src_id': cls.taxes['tax7'].id, + }) + fpos_no_tax_dest.write({ + 'account_ids': [(6, 0, account_fpos.ids)], + 'tax_ids': [(6, 0, tax_fpos.ids)], + }) + return fpos_no_tax_dest + + def test_01_no_invoice_fpos(self): + """ orders without invoice + + Orders + ====== + +---------+----------+---------------+----------+-----+---------+-----------------+--------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +---------+----------+---------------+----------+-----+---------+-----------------+--------+ + | order 1 | cash | yes, customer | product1 | 10 | 109.90 | 18.68 [7%->17%] | 128.58 | + | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.90 | + | | | | product3 | 10 | 309.90 | 52.68 [7%->17%] | 362.58 | + +---------+----------+---------------+----------+-----+---------+-----------------+--------+ + | order 2 | cash | yes, customer | product1 | 5 | 54.95 | 9.34 [7%->17%] | 64.29 | + | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + +---------+----------+---------------+----------+-----+---------+-----------------+--------+ + | order 3 | bank | no | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + | | | | product3 | 5 | 154.95 | 10.85 [7%] | 165.8 | + +---------+----------+---------------+----------+-----+---------+-----------------+--------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale_account | -154.95 | (for the 7% base amount) + | sale_account | -90.86 | (for the 10% base amount) + | other_sale_account | -474.75 | (for the 17% base amount) + | other_sale_account | -272.59 | (for the 10% base amount) + | tax 17% | -80.70 | + | tax 10% | -36.35 | + | tax 7% | -10.85 | + | pos receivable bank | 265.75 | + | pos receivable cash | 855.30 | + +---------------------+---------+ + | Total balance | 0.0 | + +---------------------+---------+ + """ + + self.customer.write({'property_account_position_id': self.fpos.id}) + self.open_new_session() + + # create orders + orders = [] + orders.append(self.create_ui_order_data( + [(self.product1, 10), (self.product2, 10), (self.product3, 10)], + customer=self.customer + )) + orders.append(self.create_ui_order_data( + [(self.product1, 5), (self.product2, 5)], + customer=self.customer, + )) + orders.append(self.create_ui_order_data( + [(self.product2, 5), (self.product3, 5)], + payments=[(self.bank_pm, 265.75)], + )) + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_move = self.pos_session.move_id + + sale_account_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + lines_balance = [-154.95, -90.86] + self.assertEqual(len(sale_account_lines), len(lines_balance)) + for balance, amount in zip(sorted(sale_account_lines.mapped('balance')), sorted(lines_balance)): + self.assertAlmostEqual(balance, amount) + + other_sale_account_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.other_sale_account) + lines_balance = [-474.75, -272.59] + self.assertEqual(len(other_sale_account_lines), len(lines_balance)) + for balance, amount in zip(sorted(other_sale_account_lines.mapped('balance')), sorted(lines_balance)): + self.assertAlmostEqual(balance, amount) + + receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertAlmostEqual(receivable_line_bank.balance, 265.75) + + receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 855.3) + + manually_calculated_taxes = (-80.7, -36.35, -10.85) + tax_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + self.assertAlmostEqual(len(manually_calculated_taxes), len(tax_lines.mapped('balance'))) + for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): + self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined.') + + self.assertTrue(receivable_line_cash.full_reconcile_id) + + def test_02_no_invoice_fpos_no_tax_dest(self): + """ Customer with fiscal position that maps a tax to no tax. + + Orders + ====== + +---------+----------+---------------+----------+-----+---------+-------------+--------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +---------+----------+---------------+----------+-----+---------+-------------+--------+ + | order 1 | bank | yes, customer | product1 | 10 | 109.90 | 0 | 109.90 | + | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.90 | + | | | | product3 | 10 | 309.90 | 0 | 309.90 | + +---------+----------+---------------+----------+-----+---------+-------------+--------+ + | order 2 | cash | yes, customer | product1 | 5 | 54.95 | 0 | 54.95 | + | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + +---------+----------+---------------+----------+-----+---------+-------------+--------+ + | order 3 | bank | no | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + | | | | product3 | 5 | 154.95 | 10.85 [7%] | 165.80 | + +---------+----------+---------------+----------+-----+---------+-------------+--------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | sale_account | -154.95 | (for the 7% base amount) + | sale_account | -90.86 | (for the 10% base amount) + | other_sale_account | -272.59 | (for the 10% base amount) + | other_sale_account | -474.75 | (no tax) + | tax 10% | -36.35 | + | tax 7% | -10.85 | + | pos receivable bank | 885.45 | + | pos receivable cash | 154.9 | + +---------------------+---------+ + | Total balance | 0.0 | + +---------------------+---------+ + """ + + self.customer.write({'property_account_position_id': self.fpos_no_tax_dest.id}) + self.open_new_session() + # create orders + orders = [] + orders.append(self.create_ui_order_data( + [(self.product1, 10), (self.product2, 10), (self.product3, 10)], + customer=self.customer, + payments=[(self.bank_pm, 619.7)], + )) + orders.append(self.create_ui_order_data( + [(self.product1, 5), (self.product2, 5)], + customer=self.customer, + )) + orders.append(self.create_ui_order_data( + [(self.product2, 5), (self.product3, 5)], + payments=[(self.bank_pm, 265.75)], + )) + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_move = self.pos_session.move_id + + sale_account_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + lines_balance = [-154.95, -90.86] + self.assertEqual(len(sale_account_lines), len(lines_balance)) + for balance, amount in zip(sorted(sale_account_lines.mapped('balance')), sorted(lines_balance)): + self.assertAlmostEqual(balance, amount) + + other_sale_account_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.other_sale_account) + lines_balance = [-474.75, -272.59] + self.assertEqual(len(other_sale_account_lines), len(lines_balance)) + for balance, amount in zip(sorted(other_sale_account_lines.mapped('balance')), sorted(lines_balance)): + self.assertAlmostEqual(balance, amount) + + receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertAlmostEqual(receivable_line_bank.balance, 885.45) + + receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 154.9) + + manually_calculated_taxes = [-36.35, -10.85] + tax_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + self.assertAlmostEqual(len(manually_calculated_taxes), len(tax_lines.mapped('balance'))) + for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): + self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined.') + + self.assertTrue(receivable_line_cash.full_reconcile_id) + + def test_03_invoiced_fpos(self): + """ Invoice 2 orders. + + Orders + ====== + +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ + | order | payments | invoiced? | product | qty | untaxed | tax | total | + +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ + | order 1 | bank | yes, customer | product1 | 10 | 109.90 | 18.68 [7%->17%] | 128.58 | + | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.90 | + | | | | product3 | 10 | 309.90 | 52.68 [7%->17%] | 362.58 | + +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ + | order 2 | cash | no, customer | product1 | 5 | 54.95 | 9.34 [7%->17%] | 64.29 | + | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ + | order 3 | cash | yes, other_customer | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | + | | | | product3 | 5 | 154.95 | 10.85 [7%] | 165.80 | + +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ + + Expected Result + =============== + +---------------------+---------+ + | account | balance | + +---------------------+---------+ + | other_sale_account | -54.95 | (for the 17% base amount) + | other_sale_account | -90.86 | (for the 10% base amount) + | tax 10% | -9.09 | + | tax 17% | -9.34 | + | pos receivable cash | 429.99 | + | pos receivable bank | 691.06 | + | receivable | -691.06 | + | other receivable | -265.75 | + +---------------------+---------+ + | Total balance | 0.0 | + +---------------------+---------+ + """ + + self.customer.write({'property_account_position_id': self.fpos.id}) + self.open_new_session() + # create orders + orders = [] + uid1 = self.create_random_uid() + orders.append(self.create_ui_order_data( + [(self.product1, 10), (self.product2, 10), (self.product3, 10)], + customer=self.customer, + payments=[(self.bank_pm, 691.06)], + is_invoiced=True, + uid=uid1 + )) + orders.append(self.create_ui_order_data( + [(self.product1, 5), (self.product2, 5)], + customer=self.customer, + )) + uid2 = self.create_random_uid() + orders.append(self.create_ui_order_data( + [(self.product2, 5), (self.product3, 5)], + customer=self.other_customer, + is_invoiced=True, + uid=uid2, + )) + # sync orders + order = self.env['pos.order'].create_from_ui(orders) + + # check values before closing the session + self.assertEqual(3, self.pos_session.order_count) + orders_total = sum(order.amount_total for order in self.pos_session.order_ids) + self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') + + invoiced_order_1 = self.pos_session.order_ids.filtered(lambda order: uid1 in order.pos_reference) + invoiced_order_2 = self.pos_session.order_ids.filtered(lambda order: uid2 in order.pos_reference) + + self.assertTrue(invoiced_order_1, msg='Invoiced order 1 should exist.') + self.assertTrue(invoiced_order_2, msg='Invoiced order 2 should exist.') + self.assertTrue(invoiced_order_1.account_move, msg='Invoiced order 1 should have invoice (account_move).') + self.assertTrue(invoiced_order_2.account_move, msg='Invoiced order 2 should have invoice (account_move).') + + # NOTE Tests of values in the invoice accounting lines is not done here. + + # close the session + self.pos_session.action_pos_session_validate() + + # check values after the session is closed + session_move = self.pos_session.move_id + + sale_account_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.sale_account) + self.assertFalse(sale_account_lines, msg='There should be no self.sale_account lines.') + + other_sale_account_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.other_sale_account) + lines_balance = [-54.95, -90.86] + self.assertEqual(len(other_sale_account_lines), len(lines_balance)) + for balance, amount in zip(sorted(other_sale_account_lines.mapped('balance')), sorted(lines_balance)): + self.assertAlmostEqual(balance, amount) + + receivable_line_bank = session_move.line_ids.filtered(lambda line: self.bank_pm.name in line.name) + self.assertAlmostEqual(receivable_line_bank.balance, 691.06) + + receivable_line_cash = session_move.line_ids.filtered(lambda line: self.cash_pm.name in line.name) + self.assertAlmostEqual(receivable_line_cash.balance, 429.99) + + manually_calculated_taxes = [-9.09, -9.34] + tax_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) + self.assertAlmostEqual(len(manually_calculated_taxes), len(tax_lines.mapped('balance'))) + for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): + self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined.') + + receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.receivable_account) + self.assertAlmostEqual(receivable_line.balance, -691.06, msg='That is not the correct receivable line balance.') + + other_receivable_line = session_move.line_ids.filtered(lambda line: line.account_id == self.other_receivable_account) + self.assertAlmostEqual(other_receivable_line.balance, -265.75, msg='That is not the correct other receivable line balance.') + + self.assertTrue(receivable_line_cash.full_reconcile_id) + self.assertTrue(receivable_line.full_reconcile_id) + self.assertTrue(other_receivable_line.full_reconcile_id) |
