import logging

from random import randint
from datetime import datetime
from odoo import fields, tools
from odoo.fields import Command
from odoo.tests import Form
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon

_logger = logging.getLogger(__name__)

def archive_products(env):
    # Archive all existing product to avoid noise during the tours
    all_pos_product = env['product.template'].search([('available_in_pos', '=', True)])
    tip = env.ref('point_of_sale.product_product_tip').product_tmpl_id
    (all_pos_product - tip)._write({'active': False})


class CommonPosTest(ValuationReconciliationTestCommon):
    @classmethod
    def setUpClass(self):
        super().setUpClass()
        archive_products(self.env)

        self.env.user.group_ids += self.env.ref('point_of_sale.group_pos_manager')
        self.env.ref('base.EUR').active = True
        self.env.ref('base.USD').active = True

        self.create_res_partners(self)
        self.create_account_cash_rounding(self)
        self.create_pos_categories(self)
        self.create_account_taxes(self)
        self.create_product_templates(self)
        self.create_payment_methods(self)
        self.create_pos_configs(self)

    def create_pos_configs(self):
        sale_journal_eur = 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,
        })
        self.pricelist_eur = self.env['product.pricelist'].create({
            'name': 'Test EUR Pricelist',
            'currency_id': self.env.ref('base.EUR').id
        })
        self.pos_config_eur = self.env['pos.config'].create({
            'name': 'PoS Config EUR',
            'journal_id': sale_journal_eur.id,
            'use_pricelist': True,
            'available_pricelist_ids': [(6, 0, self.pricelist_eur.ids)],
            'pricelist_id': self.pricelist_eur.id,
            'payment_method_ids': [(6, 0, self.bank_payment_method.ids)]
        })
        self.pos_config_usd = self.env['pos.config'].create({
            'name': 'PoS Config USD',
            'journal_id': self.company_data['default_journal_sale'].id,
            'invoice_journal_id': self.company_data['default_journal_sale'].id,
            'payment_method_ids': [
                (4, self.credit_payment_method.id),
                (4, self.bank_payment_method.id),
                (4, self.cash_payment_method.id),
            ]
        })

    def create_res_partners(self):
        self.partner_mobt = self.env['res.partner'].create({
            'name': 'MOBT',
        })
        self.partner_adgu = self.env['res.partner'].create({
            'name': 'ADGU',
        })
        self.partner_lowe = self.env['res.partner'].create({
            'name': 'LOWE',
        })
        self.partner_jcb = self.env['res.partner'].create({
            'name': 'JCB',
        })
        self.partner_moda = self.env['res.partner'].create({
            'name': 'MODA',
        })
        self.partner_stva = self.env['res.partner'].create({
            'name': 'STVA',
        })
        self.partner_manv = self.env['res.partner'].create({
            'name': 'MANV',
        })
        self.partner_vlst = self.env['res.partner'].create({
            'name': 'VLST',
        })

    def create_account_cash_rounding(self):
        self.account_cash_rounding_down = self.env['account.cash.rounding'].create({
            'name': 'Rounding down',
            'rounding': 0.05,
            'rounding_method': 'DOWN',
            'profit_account_id': self.company_data['default_account_revenue'].id,
            'loss_account_id': self.company_data['default_account_expense'].id,
        })
        self.account_cash_rounding_up = self.env['account.cash.rounding'].create({
            'name': 'Rounding up',
            'rounding': 0.05,
            'rounding_method': 'UP',
            'profit_account_id': self.company_data['default_account_revenue'].id,
            'loss_account_id': self.company_data['default_account_expense'].id,
        })
        self.account_cash_rounding_half = self.env['account.cash.rounding'].create({
            'name': 'Rounding half',
            'rounding': 0.05,
            'profit_account_id': self.company_data['default_account_revenue'].id,
            'loss_account_id': self.company_data['default_account_expense'].id,
        })

    def create_payment_methods(self):
        self.cash_payment_method = self.env['pos.payment.method'].create({
            'name': 'Cash',
            'receivable_account_id': self.company_data['default_account_receivable'].id,
            'journal_id': self.company_data['default_journal_cash'].id,
        })
        self.bank_payment_method = self.env['pos.payment.method'].create({
            'name': 'Bank',
            'journal_id': self.company_data['default_journal_bank'].id,
            'receivable_account_id': self.company_data['default_account_receivable'].id,
        })
        self.credit_payment_method = self.env['pos.payment.method'].create({
            'name': 'Credit',
            'receivable_account_id': self.company_data['default_account_receivable'].id,
            'split_transactions': True,
        })

    def create_pos_categories(self):
        self.cat_no_tax = self.env['pos.category'].create({
            'name': 'No tax',
            'sequence': 0,
        })
        self.cat_tax_five_incl = self.env['pos.category'].create({
            'name': 'Tax five incl',
            'sequence': 1,
        })
        self.cat_tax_ten_incl = self.env['pos.category'].create({
            'name': 'Tax ten incl',
            'sequence': 2,
        })
        self.cat_tax_fiften_incl = self.env['pos.category'].create({
            'name': 'Tax fifteen incl',
            'sequence': 3,
        })
        self.cat_tax_five_excl = self.env['pos.category'].create({
            'name': 'Tax five excl',
            'sequence': 4,
        })
        self.cat_tax_ten_excl = self.env['pos.category'].create({
            'name': 'Tax ten excl',
            'sequence': 5,
        })
        self.cat_tax_fiften_excl = self.env['pos.category'].create({
            'name': 'Tax fifteen excl',
            'sequence': 6,
        })

    def create_account_taxes(self):
        self.tax_five_incl = self.env['account.tax'].create({
            'name': 'Tax five incl',
            'amount': 5,
            'price_include_override': 'tax_included',
        })
        self.tax_ten_incl = self.env['account.tax'].create({
            'name': 'Tax ten incl',
            'amount': 10,
            'price_include_override': 'tax_included',
        })
        self.tax_fiften_incl = self.env['account.tax'].create({
            'name': 'Tax fifteen incl',
            'amount': 15,
            'price_include_override': 'tax_included',
        })
        self.tax_five_excl = self.env['account.tax'].create({
            'name': 'Tax five excl',
            'amount': 5,
        })
        self.tax_ten_excl = self.env['account.tax'].create({
            'name': 'Tax ten excl',
            'amount': 10,
        })
        self.tax_fiften_excl = self.env['account.tax'].create({
            'name': 'Tax fifteen excl',
            'amount': 15,
        })

    def create_product_templates(self):
        self.ten_dollars_no_tax = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Ten dollars no tax',
            'list_price': 10.0,
            'pos_categ_ids': [(6, 0, [self.cat_no_tax.id])],
            'taxes_id': [(5, 0)],
        })
        self.twenty_dollars_no_tax = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Twenty dollars no tax',
            'list_price': 20.0,
            'pos_categ_ids': [(6, 0, [self.cat_no_tax.id])],
            'taxes_id': [(5, 0)],
        })
        self.ten_dollars_with_5_incl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Ten dollars with 5 included',
            'list_price': 10.0,
            'taxes_id': [(6, 0, [self.tax_five_incl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_five_incl.id])],
        })
        self.twenty_dollars_with_5_incl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Twenty dollars with 5 included',
            'list_price': 20.0,
            'taxes_id': [(6, 0, [self.tax_five_incl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_five_incl.id])],
        })
        self.ten_dollars_with_10_incl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Ten dollars with 10 included',
            'list_price': 10.0,
            'taxes_id': [(6, 0, [self.tax_ten_incl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_ten_incl.id])],
        })
        self.twenty_dollars_with_10_incl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Twenty dollars with 10 included',
            'list_price': 20.0,
            'taxes_id': [(6, 0, [self.tax_ten_incl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_ten_incl.id])],
        })
        self.ten_dollars_with_15_incl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Ten dollars with 15 included',
            'list_price': 10.0,
            'taxes_id': [(6, 0, [self.tax_fiften_incl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_fiften_incl.id])],
        })
        self.twenty_dollars_with_15_incl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Twenty dollars with 15 included',
            'list_price': 20.0,
            'taxes_id': [(6, 0, [self.tax_fiften_incl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_fiften_incl.id])],
        })
        self.ten_dollars_with_5_excl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Ten dollars with 5 excluded',
            'list_price': 10.0,
            'taxes_id': [(6, 0, [self.tax_five_excl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_five_excl.id])],
        })
        self.twenty_dollars_with_5_excl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Twenty dollars with 5 excluded',
            'list_price': 20.0,
            'taxes_id': [(6, 0, [self.tax_five_excl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_five_excl.id])],
        })
        self.ten_dollars_with_10_excl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Ten dollars with 10 excluded',
            'list_price': 10.0,
            'taxes_id': [(6, 0, [self.tax_ten_excl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_ten_excl.id])],
        })
        self.twenty_dollars_with_10_excl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Twenty dollars with 10 excluded',
            'list_price': 20.0,
            'taxes_id': [(6, 0, [self.tax_ten_excl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_ten_excl.id])],
        })
        self.ten_dollars_with_15_excl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Ten dollars with 15 excluded',
            'list_price': 10.0,
            'taxes_id': [(6, 0, [self.tax_fiften_excl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_fiften_excl.id])],
        })
        self.twenty_dollars_with_15_excl = self.env['product.template'].create({
            'available_in_pos': True,
            'name': 'Twenty dollars with 15 excluded',
            'list_price': 20.0,
            'taxes_id': [(6, 0, [self.tax_fiften_excl.id])],
            'pos_categ_ids': [(6, 0, [self.cat_tax_fiften_excl.id])],
        })

    def create_backend_pos_order(self, data):
        pos_config = data.get('pos_config', self.pos_config_usd)
        order_data = data.get('order_data', {})
        line_product_ids = [line_data['product_id'] for line_data in data.get('line_data', [])]
        product_by_id = {p.id: p for p in self.env['product.product'].browse(line_product_ids)}
        refund = False

        if not pos_config.current_session_id:
            pos_config.open_ui()

        order = self.env['pos.order'].create({
            'amount_total': 0,
            'amount_paid': 0,
            'amount_tax': 0,
            'amount_return': 0,
            'date_order': fields.Datetime.to_string(fields.Datetime.now()),
            'company_id': self.env.company.id,
            'session_id': pos_config.current_session_id.id,
            'lines': [
                Command.create({
                    'price_unit': product_by_id[line_data['product_id']].lst_price,
                    'price_subtotal': product_by_id[line_data['product_id']].lst_price,
                    'tax_ids': [(6, 0, product_by_id[line_data['product_id']].taxes_id.ids)],
                    'price_subtotal_incl': 0,
                    **line_data,
                }) for line_data in data.get('line_data', [])
            ],
            **order_data,
        })

        # Re-trigger prices computation
        order.lines._onchange_amount_line_all()
        order._compute_prices()

        if data.get('payment_data'):
            payment_context = {"active_ids": order.ids, "active_id": order.id}
            for payment in data['payment_data']:
                make_payment = {'payment_method_id': payment['payment_method_id']}
                if payment.get('amount'):
                    make_payment['amount'] = payment['amount']
                order_payment = self.env['pos.make.payment'].with_context(**payment_context).create(make_payment)
                order_payment.with_context(**payment_context).check()

        if data.get('refund_data'):
            refund_action = order.refund()
            refund = self.env['pos.order'].browse(refund_action['res_id'])
            payment_context = {"active_ids": refund.ids, "active_id": refund.id}

            if data.get('order_data') and data['order_data'].get('to_invoice', False):
                refund.to_invoice = True

            for refund_data in data['refund_data']:
                make_refund = {'payment_method_id': refund_data['payment_method_id']}
                if refund_data.get('amount'):
                    make_refund['amount'] = refund_data['amount']
                refund_payment = self.env['pos.make.payment'].with_context(**payment_context).create(make_refund)
                refund_payment.with_context(**payment_context).check()

        return order, refund

    def compute_tax(self, product, price, qty=1, taxes=None, pos_config=None):
        config = pos_config or self.pos_config_usd
        if not taxes:
            taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id)
        currency = config.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'])


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):
        super().setUpClass()
        cls.env.user.group_ids |= cls.env.ref('point_of_sale.group_pos_manager')

        cls.company_data['company'].write({
            'point_of_sale_update_stock_quantities': 'real',
            'country_id': cls.env['res.country'].create({
                'name': 'PoS Land',
                'code': 'WOW',
            }),
        })

        # Set basic defaults
        cls.account_tax_return_journal = cls.company_data['default_tax_return_journal']
        cls.sales_account = cls.company_data['default_account_revenue']
        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,
            'account_type': 'asset_receivable',
        })
        cls.pos_receivable_account = cls.company.account_default_pos_receivable_account_id
        cls.pos_receivable_cash = cls.copy_account(cls.company.account_default_pos_receivable_account_id, {'name': 'POS Receivable Cash'})
        cls.pos_receivable_bank = cls.copy_account(cls.company.account_default_pos_receivable_account_id, {'name': 'POS Receivable Bank'})
        cls.outstanding_bank = cls.copy_account(cls.inbound_payment_method_line.payment_account_id, {'name': 'Outstanding Bank'})
        cls.c1_receivable = cls.copy_account(cls.receivable_account, {'name': 'Customer 1 Receivable'})
        cls.other_receivable_account = cls.env['account.account'].create({
            'name': 'Other Receivable',
            'code': 'RCV00',
            'account_type': 'asset_receivable',
            '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.setup_other_currency("EUR", rounding=0.001)

        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_services'
        # 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_services')
        cls.env.company.anglo_saxon_accounting = True
        cls.categ_anglo = cls._create_categ_anglo()

        # other basics
        cls.sale_account = cls.company.income_account_id
        cls.other_sale_account = cls.env['account.account'].search([
            ('company_ids', '=', cls.company.id),
            ('account_type', '=', 'income'),
            ('id', '!=', cls.sale_account.id)
        ], limit=1)

        # Set customers
        cls.customer = cls.env['res.partner'].create({'name': 'Customer 1', 'property_account_receivable_id': cls.c1_receivable.id})
        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):
        config = cls.env['pos.config'].create({
            'name': 'PoS Shop Test',
            'invoice_journal_id': cls.invoice_journal.id,
            'available_pricelist_ids': cls.currency_pricelist.ids,
            'pricelist_id': cls.currency_pricelist.id,
        })
        cls.company_data['default_journal_cash'].pos_payment_method_ids.unlink()
        cls.cash_pm1 = config.payment_method_ids.filtered(lambda c: c.journal_id.type == 'cash')
        if cls.cash_pm1:
            cls.cash_pm1.write({'receivable_account_id': cls.pos_receivable_cash.id})
        else:
            cls.cash_pm1 = cls.env['pos.payment.method'].create({
                'name': 'Cash',
                'journal_id': cls.company_data['default_journal_cash'].id,
                'receivable_account_id': cls.pos_receivable_cash.id,
                'company_id': cls.env.company.id,
            })
        cls.bank_pm1 = cls.env['pos.payment.method'].create({
            'name': 'Bank',
            'journal_id': cls.company_data['default_journal_bank'].id,
            'receivable_account_id': cls.pos_receivable_bank.id,
            'outstanding_account_id': cls.outstanding_bank.id,
            'company_id': cls.env.company.id,
        })
        cls.cash_split_pm1 = cls.cash_pm1.copy(default={
            'name': 'Split (Cash) PM',
            'split_transactions': True,
            'journal_id': cls.env['account.journal'].create({
                                'name': "Cash",
                                'code': "CSH %s" % config.id,
                                'type': 'cash',
                            }).id
        })
        cls.bank_split_pm1 = cls.bank_pm1.copy(default={
            'name': 'Split (Bank) PM',
            'split_transactions': True,
        })
        cls.pay_later_pm = cls.env['pos.payment.method'].create({'name': 'Pay Later', 'split_transactions': True})
        config.write({'payment_method_ids': [(4, cls.cash_split_pm1.id), (4, cls.bank_split_pm1.id), (4, cls.cash_pm1.id), (4, cls.bank_pm1.id), (4, cls.pay_later_pm.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,
            'name': datetime.today().date(),
        })
        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_bank_journal = cls.env['account.journal'].create({
            'name': 'Bank Other',
            'type': 'bank',
            'company_id': cls.company.id,
            'code': 'BNKO',
            'sequence': 13,
            'currency_id': cls.other_currency.id
        })
        other_pricelist = cls.env['product.pricelist'].create({
            'name': 'Public Pricelist Other',
            'currency_id': cls.other_currency.id,
        })
        cls.cash_pm2 = cls.env['pos.payment.method'].create({
            'name': 'Cash Other',
            'journal_id': other_cash_journal.id,
            'receivable_account_id': cls.pos_receivable_cash.id,
        })
        cls.bank_pm2 = cls.env['pos.payment.method'].create({
            'name': 'Bank Other',
            'journal_id': other_bank_journal.id,
            'receivable_account_id': cls.pos_receivable_bank.id,
            'outstanding_account_id': cls.outstanding_bank.id,
        })

        config = cls.env['pos.config'].create({
            'name': 'Shop Other',
            'invoice_journal_id': other_invoice_journal.id,
            'journal_id': other_sales_journal.id,
            'use_pricelist': True,
            'available_pricelist_ids': other_pricelist.ids,
            'pricelist_id': other_pricelist.id,
            'payment_method_ids': [cls.cash_pm2.id, cls.bank_pm2.id],
        })
        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_valuation_account_id': cls.company_data['default_account_stock_valuation'].copy().id
        })

    @classmethod
    def _create_taxes(cls):
        """ Create taxes

        tax7: 7%, excluded in product price
        tax10: 10%, included in product price
        tax21: 21%, included in product price
        """
        def create_tag(name):
            return cls.env['account.account.tag'].create({
                'name': name,
                'applicability': 'taxes',
                'country_id': cls.env.company.account_fiscal_country_id.id
            })

        cls.tax_tag_invoice_base = create_tag('Invoice Base tag')
        cls.tax_tag_invoice_tax = create_tag('Invoice Tax tag')
        cls.tax_tag_refund_base = create_tag('Refund Base tag')
        cls.tax_tag_refund_tax = create_tag('Refund Tax tag')

        def create_tax(percentage, price_include_override='tax_excluded', include_base_amount=False):
            return cls.env['account.tax'].create({
                'name': f'Tax {percentage}%',
                'amount': percentage,
                'price_include_override': price_include_override,
                'amount_type': 'percent',
                'include_base_amount': include_base_amount,
                'invoice_repartition_line_ids': [
                    (0, 0, {
                        'repartition_type': 'base',
                        'tag_ids': [(6, 0, cls.tax_tag_invoice_base.ids)],
                    }),
                    (0, 0, {
                        'repartition_type': 'tax',
                        'account_id': cls.tax_received_account.id,
                        'tag_ids': [(6, 0, cls.tax_tag_invoice_tax.ids)],
                    }),
                ],
                'refund_repartition_line_ids': [
                    (0, 0, {
                        'repartition_type': 'base',
                        'tag_ids': [(6, 0, cls.tax_tag_refund_base.ids)],
                    }),
                    (0, 0, {
                        'repartition_type': 'tax',
                        'account_id': cls.tax_received_account.id,
                        'tag_ids': [(6, 0, cls.tax_tag_refund_tax.ids)],
                    }),
                ],
            })

        def create_tax_fixed(amount, price_include_override='tax_excluded', include_base_amount=False):
            return cls.env['account.tax'].create({
                'name': f'Tax fixed amount {amount}',
                'amount': amount,
                'price_include_override': price_include_override,
                'include_base_amount': include_base_amount,
                'amount_type': 'fixed',
                'invoice_repartition_line_ids': [
                    (0, 0, {
                        'repartition_type': 'base',
                        'tag_ids': [(6, 0, cls.tax_tag_invoice_base.ids)],
                    }),
                    (0, 0, {
                        'repartition_type': 'tax',
                        'account_id': cls.tax_received_account.id,
                        'tag_ids': [(6, 0, cls.tax_tag_invoice_tax.ids)],
                    }),
                ],
                'refund_repartition_line_ids': [
                    (0, 0, {
                        'repartition_type': 'base',
                        'tag_ids': [(6, 0, cls.tax_tag_refund_base.ids)],
                    }),
                    (0, 0, {
                        'repartition_type': 'tax',
                        'account_id': cls.tax_received_account.id,
                        'tag_ids': [(6, 0, cls.tax_tag_refund_tax.ids)],
                    }),
                ],
            })

        tax_fixed006 = create_tax_fixed(0.06, price_include_override='tax_included', include_base_amount=True)
        tax_fixed012 = create_tax_fixed(0.12, price_include_override='tax_included', include_base_amount=True)
        tax7 = create_tax(7, price_include_override='tax_excluded')
        tax8 = create_tax(8, include_base_amount=True)
        tax9 = create_tax(9)
        tax10 = create_tax(10, price_include_override='tax_included')
        tax21 = create_tax(21, price_include_override='tax_included')


        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,
            'tax8': tax8,
            'tax9': tax9,
            'tax10': tax10,
            'tax21': tax21,
            'tax_fixed006': tax_fixed006,
            'tax_fixed012': tax_fixed012,
            '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, pos_order_lines_ui_args, pos_order_ui_args={}, customer=False, is_invoiced=False, payments=None, uuid=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) pos_order_lines_ui_args: pairs of `ordered product` and `quantity`
        or triplet of `ordered product`, `quantity` and discount
        :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 normalize_order_line_param(param):
            if isinstance(param, dict):
                return param

            assert len(param) >= 2
            return {
                'product': param[0],
                'quantity': param[1],
                'discount': 0.0 if len(param) == 2 else param[2],
            }

        def create_order_line(product, quantity, **kwargs):
            price_unit = self.pricelist._get_product_price(product, quantity)
            tax_ids = fiscal_position.map_tax(product.taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(self.env.company)))
            discount = kwargs.get('discount', 0.0)
            price_unit_after_discount = price_unit * (1 - discount / 100.0)
            tax_values = (
                tax_ids.compute_all(price_unit_after_discount, self.currency, quantity)
                if tax_ids
                else {
                    'total_excluded': price_unit * quantity,
                    'total_included': price_unit * quantity,
                }
            )
            return (0, 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)],
                **kwargs
            })

        def create_payment(payment_method, amount):
            return (0, 0, {
                'amount': amount,
                'name': fields.Datetime.now(),
                'payment_method_id': payment_method.id,
            })

        uuid = uuid or self.create_random_uid()

        # 1. generate the order lines
        order_lines = [
            create_order_line(**normalize_order_line_param(param))
            for param in pos_order_lines_ui_args
        ]

        # 2. generate the payments
        total_amount_incl = sum(line[2]['price_subtotal_incl'] for line in order_lines)
        if payments is None:
            default_cash_pm = self.config.payment_method_ids.filtered(lambda pm: pm.is_cash_count and not pm.split_transactions)[:1]
            if not default_cash_pm:
                raise Exception('There should be a cash payment method set in the pos.config.')
            payments = [create_payment(default_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 {
            '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,
            'date_order': fields.Datetime.to_string(fields.Datetime.now()),
            'fiscal_position_id': fiscal_position.id,
            'pricelist_id': self.config.pricelist_id.id,
            'name': 'Order %s' % uuid,
            'last_order_preparation_change': '{}',
            'lines': order_lines,
            'partner_id': customer and customer.id,
            'session_id': self.pos_session.id,
            'payment_ids': payments,
            'uuid': uuid,
            'user_id': self.env.uid,
            'to_invoice': is_invoiced,
            **pos_order_ui_args,
        }

    @classmethod
    def create_product(cls, name, category, lst_price, standard_price=None, tax_ids=None, sale_account=None):
        product = cls.env['product.product'].create({
            'is_storable': True,
            '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,
            'company_id': cls.env.company.id,
        })
        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
        """
        for product, qty in zip(products, quantities):
            cls.env['stock.quant'].with_context(inventory_mode=True).create({
                'product_id': product.id,
                'inventory_quantity': qty,
                'location_id': cls.stock_location_components.id,
            }).action_apply_inventory()

    def open_new_session(self, opening_cash=0):
        """ 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.
            * pos_session : the current_session_id of config
            * currency : currency of the current pos.session
            * pricelist : the default pricelist of the session
        """
        self.config.open_ui()
        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.pos_session.set_opening_control(opening_cash, None)
        return self.pos_session

    def _run_test(self, args):
        pos_session = self._start_pos_session(args['payment_methods'], args.get('opening_cash', 0))
        _logger.info('DONE: Start session.')
        orders_map = self._create_orders(args['orders'])
        _logger.info('DONE: Orders created.')
        before_closing_cb = args.get('before_closing_cb')
        if before_closing_cb:
            before_closing_cb()
            _logger.info('DONE: Call of before_closing_cb.')
        self._check_invoice_journal_entries(pos_session, orders_map, expected_values=args['journal_entries_before_closing'])
        _logger.info('DONE: Checks for journal entries before closing the session.')
        cash_payment_method = pos_session.payment_method_ids.filtered('is_cash_count')[:1]
        total_cash_payment = sum(pos_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.id == cash_payment_method.id).mapped('amount'))
        pos_session.post_closing_cash_details(total_cash_payment)
        pos_session.close_session_from_ui()
        after_closing_cb = args.get('after_closing_cb')
        if after_closing_cb:
            after_closing_cb()
            _logger.info('DONE: Call of after_closing_cb.')
        self._check_session_journal_entries(pos_session, expected_values=args['journal_entries_after_closing'])
        _logger.info('DONE: Checks for journal entries after closing the session.')

    def _start_pos_session(self, payment_methods, opening_cash):
        self.config.write({'payment_method_ids': [(6, 0, payment_methods.ids)]})
        pos_session = self.open_new_session(opening_cash)
        self.assertEqual(self.config.payment_method_ids.ids, pos_session.payment_method_ids.ids, msg='Payment methods in the config should be the same as the session.')
        return pos_session

    def _create_orders(self, order_data_params):
        '''Returns a dict mapping uuid to its created pos.order record.'''
        result = {}
        order_data = [self.create_ui_order_data(**params) for params in order_data_params]
        order_ids = [order['id'] for order in self.env['pos.order'].sync_from_ui(order_data)['pos.order']]
        for order_id in self.env["pos.order"].browse(order_ids):
            result[order_id.uuid] = order_id
        return result

    def _check_invoice_journal_entries(self, pos_session, orders_map, expected_values):
        '''Checks the invoice, together with the payments, from each invoiced order.'''
        currency_rounding = pos_session.currency_id.rounding

        for uid in orders_map:
            order = orders_map[uid]
            if not order.is_invoiced:
                continue
            invoice = order.account_move
            # allow not checking the invoice since pos is not creating the invoices
            if expected_values[uid].get('invoice'):
                self._assert_account_move(invoice, expected_values[uid]['invoice'])
                _logger.info('DONE: Check of invoice for order %s.', uid)

            for pos_payment in order.payment_ids:
                if pos_payment.payment_method_id == self.pay_later_pm:
                    # Skip the pay later payments since there are no journal entries
                    # for them when invoicing.
                    continue

                # This predicate is used to match the pos_payment's journal entry to the
                # list of payments specified in the 'payments' field of the `_run_test`
                # args.
                def predicate(args):
                    payment_method, amount = args
                    first = payment_method == pos_payment.payment_method_id
                    second = tools.float_is_zero(pos_payment.amount - amount, precision_rounding=currency_rounding)
                    return first and second

                self._find_then_assert_values(pos_payment.account_move_id, expected_values[uid]['payments'], predicate)
                _logger.info('DONE: Check of invoice payment (%s, %s) for order %s.', pos_payment.payment_method_id.name, pos_payment.amount, uid)

    def _check_session_journal_entries(self, pos_session, expected_values):
        '''Checks the journal entries after closing the session excluding entries checked in `_check_invoice_journal_entries`.'''
        currency_rounding = pos_session.currency_id.rounding

        # check expected session journal entry
        self._assert_account_move(pos_session.move_id, expected_values['session_journal_entry'])
        _logger.info("DONE: Check of the session's account move.")

        # check expected cash journal entries
        for statement_line in pos_session.statement_line_ids:
            def statement_line_predicate(args):
                return tools.float_is_zero(statement_line.amount - args[0], precision_rounding=currency_rounding)
            self._find_then_assert_values(statement_line.move_id, expected_values['cash_statement'], statement_line_predicate)
        _logger.info("DONE: Check of cash statement lines.")

        # check expected bank payments
        for bank_payment in pos_session.bank_payment_ids:
            def bank_payment_predicate(args):
                return tools.float_is_zero(bank_payment.amount - args[0], precision_rounding=currency_rounding)
            self._find_then_assert_values(bank_payment.move_id, expected_values['bank_payments'], bank_payment_predicate)
        _logger.info("DONE: Check of bank account payments.")

    def _find_then_assert_values(self, account_move, source_of_expected_vals, predicate):
        expected_move_vals = next(move_vals for args, move_vals in source_of_expected_vals if predicate(args))
        self._assert_account_move(account_move, expected_move_vals)

    def _assert_account_move(self, account_move, expected_account_move_vals):
        if expected_account_move_vals:
            # We allow partial checks of the lines of the account move if `line_ids_predicate` is specified.
            # This means that only those that satisfy the predicate are compared to the expected account move line_ids.
            line_ids_predicate = expected_account_move_vals.pop('line_ids_predicate', lambda _: True)
            line_ids = expected_account_move_vals.pop('line_ids')
            reconciliation_statuses = []
            for line in line_ids:
                partially_reconciled = line.pop('partially_reconciled', False)
                if partially_reconciled is True:
                    reconciliation_statuses.append('partially_reconciled')
                else:
                    reconciliation_statuses.append('fully_reconciled' if line.get('reconciled') else 'not_reconciled')
            account_move_line_ids = account_move.line_ids.filtered(line_ids_predicate)
            self.assertRecordValues(account_move_line_ids, line_ids)
            self.assertRecordValues(account_move, [expected_account_move_vals])

            # Check reconciliation status
            for line, reconciliation_status in zip(account_move_line_ids, reconciliation_statuses):
                # See 'account_move_line._compute_amount_residual'  for more explanation
                if reconciliation_status == 'fully_reconciled':
                    if line.matching_number:
                        self.assertTrue(line.full_reconcile_id)
                    self.assertAlmostEqual(line.amount_residual, 0)
                elif reconciliation_status == 'partially_reconciled':
                    self.assertFalse(line.full_reconcile_id)
                    if line.reconciled:
                        self.assertAlmostEqual(line.amount_residual, 0)
                    else:
                        self.assertGreater(abs(line.amount_residual), 0)
                elif reconciliation_status == 'not_reconciled':
                    self.assertFalse(line.full_reconcile_id)
                    self.assertFalse(line.reconciled)
        else:
            # if the expected_account_move_vals is falsy, the account_move should be falsy.
            self.assertFalse(account_move)

    def make_payment(self, order, payment_method, amount):
        """ Make payment for the order using the given payment method.
        """
        payment_context = {"active_id": order.id, "active_ids": order.ids}
        return self.env['pos.make.payment'].with_context(**payment_context).create({
            'amount': amount,
            'payment_method_id': payment_method.id,
        }).check()
