summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/tests/common.py
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/common.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/point_of_sale/tests/common.py')
-rw-r--r--addons/point_of_sale/tests/common.py523
1 files changed, 523 insertions, 0 deletions
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]