summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/point_of_sale/tests')
-rw-r--r--addons/point_of_sale/tests/__init__.py17
-rw-r--r--addons/point_of_sale/tests/common.py523
-rw-r--r--addons/point_of_sale/tests/test_anglo_saxon.py224
-rw-r--r--addons/point_of_sale/tests/test_frontend.py525
-rw-r--r--addons/point_of_sale/tests/test_js.py22
-rw-r--r--addons/point_of_sale/tests/test_point_of_sale.py63
-rw-r--r--addons/point_of_sale/tests/test_point_of_sale_flow.py1008
-rw-r--r--addons/point_of_sale/tests/test_point_of_sale_ui.py16
-rw-r--r--addons/point_of_sale/tests/test_pos_basic_config.py662
-rw-r--r--addons/point_of_sale/tests/test_pos_multiple_receivable_accounts.py204
-rw-r--r--addons/point_of_sale/tests/test_pos_multiple_sale_accounts.py228
-rw-r--r--addons/point_of_sale/tests/test_pos_other_currency_config.py292
-rw-r--r--addons/point_of_sale/tests/test_pos_products_with_tax.py295
-rw-r--r--addons/point_of_sale/tests/test_pos_setup.py76
-rw-r--r--addons/point_of_sale/tests/test_pos_stock_account.py249
-rw-r--r--addons/point_of_sale/tests/test_pos_with_fiscal_position.py390
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)