diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/tests/common.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/point_of_sale/tests/common.py')
| -rw-r--r-- | addons/point_of_sale/tests/common.py | 523 |
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] |
