odoo18/addons/point_of_sale/tests/test_frontend.py

2209 lines
88 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from contextlib import contextmanager
from unittest.mock import patch
from odoo import Command, api
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
from odoo.tests import tagged
from odoo.addons.account.tests.common import TestTaxCommon, AccountTestInvoicingHttpCommon
from odoo.addons.point_of_sale.tests.common_setup_methods import setup_product_combo_items
from datetime import date, timedelta
from odoo.addons.point_of_sale.tests.common import archive_products
from odoo.exceptions import UserError
from odoo.addons.point_of_sale.models.pos_config import PosConfig
_logger = logging.getLogger(__name__)
class TestPointOfSaleHttpCommon(AccountTestInvoicingHttpCommon):
@classmethod
def _get_main_company(cls):
return cls.company_data['company']
def _get_url(self, pos_config=None):
pos_config = pos_config or self.main_pos_config
return f"/pos/ui?config_id={pos_config.id}"
def start_pos_tour(self, tour_name, login="pos_user", **kwargs):
self.start_tour(self._get_url(pos_config=kwargs.get('pos_config')), tour_name, login=login, **kwargs)
@contextmanager
def with_new_session(self, config=None, user=None):
config = config or self.main_pos_config
user = user or self.pos_user
config.with_user(user).open_ui()
session = config.current_session_id
yield session
session.post_closing_cash_details(0)
session.close_session_from_ui()
@classmethod
def setUpClass(cls):
super().setUpClass()
env = cls.env
cls.env.user.groups_id += env.ref('point_of_sale.group_pos_manager')
journal_obj = env['account.journal']
account_obj = env['account.account']
main_company = cls._get_main_company()
cls.account_receivable = account_obj.create({'code': 'X1012',
'name': 'Account Receivable - Test',
'account_type': 'asset_receivable',
'reconcile': True})
env.company.account_default_pos_receivable_account_id = cls.account_receivable
env['ir.default'].set('res.partner', 'property_account_receivable_id', cls.account_receivable.id, company_id=main_company.id)
# Pricelists are set below, do not take demo data into account
env['res.partner'].sudo().invalidate_model(['property_product_pricelist', 'specific_property_product_pricelist'])
# remove the all specific values for all companies only for test
env.cr.execute('UPDATE res_partner SET specific_property_product_pricelist = NULL')
# Create user.
cls.pos_user = cls.env['res.users'].create({
'name': 'A simple PoS man!',
'login': 'pos_user',
'password': 'pos_user',
'groups_id': [
(4, cls.env.ref('base.group_user').id),
(4, cls.env.ref('point_of_sale.group_pos_user').id),
(4, cls.env.ref('stock.group_stock_user').id),
],
'tz': 'America/New_York',
})
cls.pos_admin = cls.env['res.users'].create({
'name': 'A powerful PoS man!',
'login': 'pos_admin',
'password': 'pos_admin',
'groups_id': [
(4, cls.env.ref('point_of_sale.group_pos_manager').id),
],
'tz': 'America/New_York',
})
cls.pos_user.partner_id.email = 'pos_user@test.com'
cls.pos_admin.partner_id.email = 'pos_admin@test.com'
cls.bank_journal = journal_obj.create({
'name': 'Bank Test',
'type': 'bank',
'company_id': main_company.id,
'code': 'BNK',
'sequence': 10,
})
cls.bank_payment_method = env['pos.payment.method'].create({
'name': 'Bank',
'journal_id': cls.bank_journal.id,
'outstanding_account_id': cls.inbound_payment_method_line.payment_account_id.id,
})
env['pos.config'].search([]).unlink()
cls.main_pos_config = env['pos.config'].create({
'name': 'Shop',
'module_pos_restaurant': False,
})
env['res.partner'].create({
'name': 'Deco Addict',
})
cash_journal = journal_obj.create({
'name': 'Cash Test',
'type': 'cash',
'company_id': main_company.id,
'code': 'CSH',
'sequence': 10,
})
archive_products(env)
cls.tip = env.ref('point_of_sale.product_product_tip')
cls.pos_desk_misc_test = env['pos.category'].create({
'name': 'Misc test',
})
cls.pos_cat_chair_test = env['pos.category'].create({
'name': 'Chair test',
})
cls.pos_cat_desk_test = env['pos.category'].create({
'name': 'Desk test',
})
# test an extra price on an attribute
cls.whiteboard_pen = 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_ids': [(4, cls.pos_desk_misc_test.id)],
})
cls.wall_shelf = env['product.product'].create({
'name': 'Wall Shelf Unit',
'available_in_pos': True,
'list_price': 1.98,
'taxes_id': False,
'barcode': '2100005000000',
})
cls.small_shelf = env['product.product'].create({
'name': 'Small Shelf',
'available_in_pos': True,
'list_price': 2.83,
'taxes_id': False,
})
cls.magnetic_board = env['product.product'].create({
'name': 'Magnetic Board',
'available_in_pos': True,
'list_price': 1.98,
'taxes_id': False,
'barcode': '2305000000004',
})
cls.monitor_stand = env['product.product'].create({
'name': 'Monitor Stand',
'available_in_pos': True,
'list_price': 3.19,
'taxes_id': False,
'barcode': '0123456789', # No pattern in barcode nomenclature
})
cls.desk_pad = env['product.product'].create({
'name': 'Desk Pad',
'available_in_pos': True,
'list_price': 1.98,
'taxes_id': False,
'pos_categ_ids': [(4, cls.pos_cat_desk_test.id)],
})
cls.letter_tray = env['product.product'].create({
'name': 'Letter Tray',
'available_in_pos': True,
'list_price': 4.80,
'taxes_id': False,
'pos_categ_ids': [(4, cls.pos_cat_chair_test.id)],
})
cls.desk_organizer = env['product.product'].create({
'name': 'Desk Organizer',
'available_in_pos': True,
'list_price': 5.10,
'taxes_id': False,
'barcode': '2300002000007',
})
cls.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': cls.whiteboard_pen.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': cls.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,
})
env['product.template.attribute.line'].create({
'product_tmpl_id': cls.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_wool = env['product.attribute.value'].create({
'name': 'wool',
'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,
})
env['product.template.attribute.line'].create({
'product_tmpl_id': cls.configurable_chair.product_tmpl_id.id,
'attribute_id': chair_fabrics_attribute.id,
'value_ids': [(6, 0, [chair_fabrics_leather.id, chair_fabrics_wool.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': cls.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': cls.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': cls.wall_shelf.id,
}), (0, 0, {
'compute_price': 'percentage',
'percent_price': 99,
'applied_on': '0_product_variant',
'product_id': cls.small_shelf.id,
}), (0, 0, {
'compute_price': 'percentage',
'percent_price': 0,
'applied_on': '0_product_variant',
'product_id': cls.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': cls.wall_shelf.id,
}), (0, 0, {
# .99 prices
'compute_price': 'formula',
'price_surcharge': -0.01,
'price_round': 1,
'applied_on': '0_product_variant',
'product_id': cls.small_shelf.id,
}), (0, 0, {
'compute_price': 'formula',
'price_min_margin': 10,
'price_max_margin': 100,
'applied_on': '0_product_variant',
'product_id': cls.magnetic_board.id,
}), (0, 0, {
'compute_price': 'formula',
'price_surcharge': 10,
'price_max_margin': 5,
'applied_on': '0_product_variant',
'product_id': cls.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': cls.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': cls.wall_shelf.id,
}), (0, 0, {
'compute_price': 'fixed',
'fixed_price': 2,
'applied_on': '0_product_variant',
'min_quantity': 1,
'product_id': cls.wall_shelf.id,
}), (0, 0, {
'compute_price': 'fixed',
'fixed_price': 1,
'min_quantity': 5,
'product_tmpl_id': cls.monitor_stand.product_tmpl_id.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': cls.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,
})],
})
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 = env['res.partner'].create({
'name': 'Lumber Inc',
'is_company': True,
})
res_partner_18.property_product_pricelist = excluded_pricelist
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),
'|', ('company_id', '=', main_company.id), ('company_id', '=', False)
])
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})
cls.letter_tray.taxes_id = [(6, 0, [src_tax.id])]
cls.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',
'journal_id': cash_journal.id,
'receivable_account_id': cls.account_receivable.id,
})],
'use_pricelist': True,
'pricelist_id': public_pricelist.id,
'available_pricelist_ids': [(4, pricelist.id) for pricelist in all_pricelists],
})
# Set customers
partners = cls.env['res.partner'].create([
{'name': 'Partner Test 1'},
{'name': 'Partner Test 2'},
{'name': 'Partner Test 3'},
{
'name': 'Partner Full',
'email': 'partner.full@example.com',
'street': '77 Santa Barbara Rd',
'city': 'Pleasant Hill',
'state_id': cls.env.ref('base.state_us_5').id,
'zip': '94523',
'country_id': cls.env.ref('base.us').id,
}
])
cls.partner_test_1 = partners[0]
cls.partner_test_2 = partners[1]
cls.partner_test_3 = partners[2]
cls.partner_full = partners[3]
# Change the default sale pricelist of customers,
# so the js tests can expect deterministically this pricelist when selecting a customer.
# bad hack only for test
env['ir.default'].set("res.partner", "specific_property_product_pricelist", public_pricelist.id, company_id=main_company.id)
@tagged('post_install', '-at_install')
class TestUi(TestPointOfSaleHttpCommon):
def test_01_pos_basic_order(self):
self.tip.write({
'taxes_id': False
})
self.main_pos_config.write({
'iface_tipproduct': True,
'tip_product_id': self.tip.id,
'ship_later': True
})
# open a session, the /pos/ui controller will redirect to it
self.main_pos_config.with_user(self.pos_user).open_ui()
# 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="pos_user")
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'pos_basic_order_01_multi_payment_and_change', login="pos_user")
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'pos_basic_order_02_decimal_order_quantity', login="pos_user")
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'pos_basic_order_03_tax_position', login="pos_user")
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'FloatingOrderTour', login="pos_user")
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ProductScreenTour', login="pos_user")
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenTour', login="pos_user")
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ReceiptScreenTour', login="pos_user")
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.pos_user.write({
'groups_id': [
(4, self.env.ref('account.group_account_invoice').id),
]
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ChromeTour', login="pos_user")
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.')
last_order = self.env['pos.order'].search([], limit=1, order="id desc")
self.assertEqual(last_order.lines[0].price_subtotal, 30.0)
self.assertEqual(last_order.lines[0].price_subtotal_incl, 30.0)
def test_03_pos_with_lots(self):
# open a session, the /pos/ui controller will redirect to it
self.main_pos_config.with_user(self.pos_user).open_ui()
self.monitor_stand.tracking = 'lot'
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_03_pos_with_lots', login="pos_user")
def test_04_product_configurator(self):
# Making one attribute inactive to verify that it doesn't show
configurable_product = self.env['product.product'].search([('name', '=', 'Configurable Chair'), ('available_in_pos', '=', 'True')], limit=1)
fabrics_line = configurable_product.attribute_line_ids[2]
fabrics_line.product_template_value_ids[1].ptav_active = False
self.pos_user.write({
'groups_id': [
(4, self.env.ref('stock.group_stock_manager').id),
]
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_pos_tour('ProductConfiguratorTour')
def test_05_ticket_screen(self):
self.pos_user.write({
'groups_id': [
(4, self.env.ref('account.group_account_invoice').id),
]
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'TicketScreenTour', login="pos_user")
def test_product_information_screen_admin(self):
'''Consider this test method to contain a test tour with miscellaneous tests/checks that require admin access.
'''
self.product_a.available_in_pos = True
self.pos_admin.write({
'groups_id': [Command.link(self.env.ref('base.group_system').id)],
})
self.assertFalse(self.product_a.is_storable)
self.main_pos_config.with_user(self.pos_admin).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'CheckProductInformation', login="pos_admin")
def test_fixed_tax_negative_qty(self):
""" Assert the negative amount of a negative-quantity orderline
with zero-amount product with fixed tax.
"""
# setup the zero-amount product
tax_received_account = self.env['account.account'].create({
'name': 'TAX_BASE',
'code': 'TBASE',
'account_type': 'asset_current',
})
fixed_tax = self.env['account.tax'].create({
'name': 'fixed amount tax',
'amount_type': 'fixed',
'amount': 1,
'invoice_repartition_line_ids': [
(0, 0, {'repartition_type': 'base'}),
(0, 0, {
'repartition_type': 'tax',
'account_id': tax_received_account.id,
}),
],
'price_include_override': 'tax_excluded',
})
zero_amount_product = self.env['product.product'].create({
'name': 'Zero Amount Product',
'available_in_pos': True,
'list_price': 0,
'taxes_id': [(6, 0, [fixed_tax.id])],
})
# Make an order with the zero-amount product from the frontend.
# We need to do this because of the fix in the "compute_all" port.
self.main_pos_config.write({'iface_tax_included': 'total'})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'FixedTaxNegativeQty', login="pos_user")
pos_session = self.main_pos_config.current_session_id
# Close the session and check the session journal entry.
pos_session.action_pos_session_validate()
lines = pos_session.move_id.line_ids.sorted('balance')
# order in the tour is paid using the bank payment method.
bank_pm = self.main_pos_config.payment_method_ids.filtered(lambda pm: pm.name == 'Bank')
self.assertEqual(lines[0].account_id, bank_pm.receivable_account_id or self.env.company.account_default_pos_receivable_account_id)
self.assertAlmostEqual(lines[0].balance, -1)
self.assertEqual(lines[1].account_id, zero_amount_product.categ_id.property_account_income_categ_id)
self.assertAlmostEqual(lines[1].balance, 0)
self.assertEqual(lines[2].account_id, tax_received_account)
self.assertAlmostEqual(lines[2].balance, 1)
def test_change_without_cash_method(self):
#create bank payment method
bank_pm = self.env['pos.payment.method'].create({
'name': 'Bank',
'receivable_account_id': self.env.company.account_default_pos_receivable_account_id.id,
'is_cash_count': False,
'split_transactions': False,
'company_id': self.env.company.id,
})
self.main_pos_config.write({'payment_method_ids': [(6, 0, bank_pm.ids)], 'ship_later': True})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenTour2', login="pos_user")
def test_rounding_up(self):
rouding_method = self.env['account.cash.rounding'].create({
'name': 'Rounding up',
'rounding': 0.05,
'rounding_method': 'UP',
})
self.env['product.product'].create({
'name': 'Product Test',
'available_in_pos': True,
'list_price': 1.96,
'taxes_id': False,
})
self.main_pos_config.write({
'rounding_method': rouding_method.id,
'cash_rounding': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenRoundingUp', login="pos_user")
def test_rounding_down(self):
rouding_method = self.env['account.cash.rounding'].create({
'name': 'Rounding down',
'rounding': 0.05,
'rounding_method': 'DOWN',
})
self.env['product.product'].create({
'name': 'Product Test',
'available_in_pos': True,
'list_price': 1.98,
'taxes_id': False,
})
self.main_pos_config.write({
'rounding_method': rouding_method.id,
'cash_rounding': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenRoundingDown', login="pos_user")
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenTotalDueWithOverPayment', login="pos_user")
def test_rounding_half_up(self):
rouding_method = self.env['account.cash.rounding'].create({
'name': 'Rounding HALF-UP',
'rounding': 0.5,
'rounding_method': 'HALF-UP',
})
self.env['product.product'].create({
'name': 'Product Test 1.20',
'available_in_pos': True,
'list_price': 1.2,
'taxes_id': False,
})
self.env['product.product'].create({
'name': 'Product Test 1.25',
'available_in_pos': True,
'list_price': 1.25,
'taxes_id': False,
})
self.env['product.product'].create({
'name': 'Product Test 1.4',
'available_in_pos': True,
'list_price': 1.4,
'taxes_id': False,
})
self.main_pos_config.write({
'rounding_method': rouding_method.id,
'cash_rounding': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenRoundingHalfUp', login="pos_user")
def test_pos_closing_cash_details(self):
"""Test cash difference *loss* at closing.
"""
self.main_pos_config.open_ui()
current_session = self.main_pos_config.current_session_id
current_session.post_closing_cash_details(0)
current_session.close_session_from_ui()
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'CashClosingDetails', login="pos_user")
cash_diff_line = self.env['account.bank.statement.line'].search([
('payment_ref', 'ilike', 'Cash difference observed during the counting (Loss)')
])
self.assertAlmostEqual(cash_diff_line.amount, -1.00)
def test_cash_payments_should_reflect_on_next_opening(self):
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'OrderPaidInCash', login="pos_user")
def test_customer_note_is_present_after_refresh(self):
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'CustomerNoteIsPresentAfterRefresh', login="pos_user")
def test_serial_number_do_not_duplicate_after_refresh(self):
self.product1 = self.env['product.product'].create({
'name': 'Product A',
'is_storable': True,
'tracking': 'serial',
'categ_id': self.env.ref('product.product_category_all').id,
'available_in_pos': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_pos_tour("test_serial_number_do_not_duplicate_after_refresh")
def test_tax_control_button_visiblity(self):
self.main_pos_config.write({
'tax_regime_selection': False,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_pos_tour('test_tax_control_button_visiblity')
def test_fiscal_position_no_tax(self):
#create a tax of 15% with price included
tax = self.env['account.tax'].create({
'name': 'Tax 15%',
'amount': 15,
'price_include_override': 'tax_included',
'amount_type': 'percent',
'type_tax_use': 'sale',
})
#create a product with the tax
self.product = self.env['product.product'].create({
'name': 'Test Product',
'taxes_id': [(6, 0, [tax.id])],
'list_price': 100,
'available_in_pos': True,
})
#create a fiscal position that map the tax to no tax
fiscal_position = self.env['account.fiscal.position'].create({
'name': 'No Tax',
'tax_ids': [(0, 0, {
'tax_src_id': tax.id,
'tax_dest_id': False,
})],
})
pricelist = self.env['product.pricelist'].create({
'name': 'Test Pricelist',
})
self.main_pos_config.write({
'tax_regime_selection': True,
'fiscal_position_ids': [(6, 0, [fiscal_position.id])],
'available_pricelist_ids': [(6, 0, [pricelist.id])],
'pricelist_id': pricelist.id,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'FiscalPositionNoTax', login="pos_user")
def test_fiscal_position_inclusive_and_exclusive_tax(self):
""" Test the mapping of fiscal position for both Tax Inclusive ans Tax Exclusive"""
# create a tax with price included
tax_inclusive_1 = self.env['account.tax'].create({
'name': 'Tax incl.20%',
'amount': 20,
'price_include_override': 'tax_included',
'amount_type': 'percent',
'type_tax_use': 'sale',
})
tax_exclusive_1 = self.env['account.tax'].create({
'name': 'Tax excl.20%',
'amount': 20,
'price_include_override': 'tax_excluded',
'amount_type': 'percent',
'type_tax_use': 'sale',
})
tax_inclusive_2 = self.env['account.tax'].create({
'name': 'Tax incl.10%',
'amount': 10,
'price_include_override': 'tax_included',
'amount_type': 'percent',
'type_tax_use': 'sale',
})
tax_exclusive_2 = self.env['account.tax'].create({
'name': 'Tax excl.10%',
'amount': 10,
'price_include_override': 'tax_excluded',
'amount_type': 'percent',
'type_tax_use': 'sale',
})
self.test_product_1 = self.env['product.product'].create({
'name': 'Test Product 1',
'available_in_pos': True,
'list_price': 100,
'taxes_id': [(6, 0, [tax_inclusive_1.id])],
})
self.test_product_2 = self.env['product.product'].create({
'name': 'Test Product 2',
'available_in_pos': True,
'list_price': 100,
'taxes_id': [(6, 0, [tax_exclusive_1.id])],
})
# create a fiscal position that map the tax
fiscal_position_1 = self.env['account.fiscal.position'].create({
'name': 'Incl. to Incl.',
'tax_ids': [(0, 0, {
'tax_src_id': tax_inclusive_1.id,
'tax_dest_id': tax_inclusive_2.id,
})],
})
fiscal_position_2 = self.env['account.fiscal.position'].create({
'name': 'Incl. to Excl.',
'tax_ids': [(0, 0, {
'tax_src_id': tax_inclusive_1.id,
'tax_dest_id': tax_exclusive_2.id,
})],
})
fiscal_position_3 = self.env['account.fiscal.position'].create({
'name': 'Excl. to Excl.',
'tax_ids': [(0, 0, {
'tax_src_id': tax_exclusive_1.id,
'tax_dest_id': tax_exclusive_2.id,
})],
})
fiscal_position_4 = self.env['account.fiscal.position'].create({
'name': 'Excl. to Incl.',
'tax_ids': [(0, 0, {
'tax_src_id': tax_exclusive_1.id,
'tax_dest_id': tax_inclusive_2.id,
})],
})
# add the fiscal position to the PoS
self.main_pos_config.write({
'tax_regime_selection': True,
'fiscal_position_ids': [(6, 0, [
fiscal_position_1.id,
fiscal_position_2.id,
fiscal_position_3.id,
fiscal_position_4.id,
])],
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'FiscalPositionIncl', login="pos_user")
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'FiscalPositionExcl', login="pos_user")
def test_06_pos_discount_display_with_multiple_pricelist(self):
""" Test the discount display on the POS screen when multiple pricelists are used."""
test_product = self.env['product.product'].create({
'name': 'Test Product',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
})
base_pricelist = self.env['product.pricelist'].create({
'name': 'base_pricelist',
})
self.env['product.pricelist.item'].create({
'pricelist_id': base_pricelist.id,
'product_tmpl_id': test_product.product_tmpl_id.id,
'compute_price': 'percentage',
'applied_on': '1_product',
'percent_price': 30,
})
special_pricelist = self.env['product.pricelist'].create({
'name': 'special_pricelist',
})
self.env['product.pricelist.item'].create({
'pricelist_id': special_pricelist.id,
'base': 'pricelist',
'base_pricelist_id': base_pricelist.id,
'compute_price': 'percentage',
'applied_on': '3_global',
'percent_price': 10,
})
self.main_pos_config.write({
'pricelist_id': base_pricelist.id,
'available_pricelist_ids': [(6, 0, [base_pricelist.id, special_pricelist.id])],
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ReceiptScreenDiscountWithPricelistTour', login="pos_user")
def test_07_product_combo(self):
self.env['decimal.precision'].search([('name', '=', 'Product Price')]).digits = 4
setup_product_combo_items(self)
self.office_combo.write({
'lst_price': 50,
'barcode': 'SuperCombo',
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_pos_tour('ProductComboPriceTaxIncludedTour')
order = self.env['pos.order'].search([])
self.assertEqual(len(order.lines), 4, "There should be 4 order lines - 1 combo parent and 3 combo lines")
# check that the combo lines are correctly linked to each other
parent_line_id = self.env['pos.order.line'].search([('product_id.name', '=', 'Office Combo'), ('order_id', '=', order.id)])
combo_line_ids = self.env['pos.order.line'].search([('product_id.name', '!=', 'Office Combo'), ('order_id', '=', order.id)])
self.assertEqual(parent_line_id.combo_line_ids, combo_line_ids, "The combo parent should have 3 combo lines")
self.assertEqual(order.lines[1].price_unit, 10.33)
self.assertEqual(order.lines[2].price_unit, 18.67)
self.assertEqual(order.lines[3].price_unit, 30.00)
# In the future we might want to test also if:
# - the combo lines are correctly stored in and restored from local storage
# - the combo lines are correctly shared between the pos configs ( in cross ordering )
def test_07_pos_barcodes_scan(self):
barcode_rule = self.env.ref("point_of_sale.barcode_rule_client")
barcode_rule.pattern = barcode_rule.pattern + "|234"
# should in theory be changed in the JS code to `|^234`
# If not, it will fail as it will mistakenly match with the product barcode "0123456789"
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'BarcodeScanningTour', login="pos_user")
def test_08_show_tax_excluded(self):
# define a tax included tax record
tax = self.env['account.tax'].create({
'name': 'Tax 10% Included',
'amount_type': 'percent',
'amount': 10,
'price_include_override': 'tax_included',
})
# define a product record with the tax
self.env['product.product'].create({
'name': 'Test Product',
'list_price': 110,
'taxes_id': [(6, 0, [tax.id])],
'available_in_pos': True,
})
# set Tax-Excluded Price
self.main_pos_config.write({
'iface_tax_included': 'subtotal'
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ShowTaxExcludedTour', login="pos_user")
def test_chrome_without_cash_move_permission(self):
self.env.user.write({'groups_id': [
Command.set(
[
self.env.ref('base.group_user').id,
self.env.ref('point_of_sale.group_pos_user').id,
]
)
]})
self.main_pos_config.open_ui()
self.start_pos_tour('test_chrome_without_cash_move_permission', login='accountman')
def test_09_pos_barcodes_scan_product_pacaging(self):
product = self.env['product.product'].create({
'name': 'Packaging Product',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
'barcode': '12345601',
})
self.env['product.packaging'].create({
'name': 'Product Packaging 10 Products',
'qty': 10,
'product_id': product.id,
'barcode': '12345610',
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'BarcodeScanningProductPackagingTour', login="pos_user")
def test_GS1_pos_barcodes_scan(self):
barcodes_gs1_nomenclature = self.env.ref("barcodes_gs1_nomenclature.default_gs1_nomenclature")
self.main_pos_config.company_id.write({
'nomenclature_id': barcodes_gs1_nomenclature.id
})
self.env['product.product'].create({
'name': 'Product 1',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
'barcode': '08431673020125',
})
self.env['product.product'].create({
'name': 'Product 2',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
'barcode': '08431673020126',
})
# 3760171283370 can be parsed with GS1 rules but it's not GS1
self.env['product.product'].create({
'name': 'Product 3',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
'barcode': '3760171283370',
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'GS1BarcodeScanningTour', login="pos_user")
def test_refund_order_with_fp_tax_included(self):
#create a tax of 15% tax included
self.tax1 = self.env['account.tax'].create({
'name': 'Tax 1',
'amount': 15,
'amount_type': 'percent',
'type_tax_use': 'sale',
'price_include_override': 'tax_included',
})
#create a tax of 0%
self.tax2 = self.env['account.tax'].create({
'name': 'Tax 2',
'amount': 0,
'amount_type': 'percent',
'type_tax_use': 'sale',
'price_include_override': 'tax_included',
})
#create a fiscal position with the two taxes
self.fiscal_position = self.env['account.fiscal.position'].create({
'name': 'No Tax',
'tax_ids': [(0, 0, {
'tax_src_id': self.tax1.id,
'tax_dest_id': self.tax2.id,
})],
})
self.product_test = self.env['product.product'].create({
'name': 'Product Test',
'is_storable': True,
'available_in_pos': True,
'list_price': 100,
'taxes_id': [(6, 0, self.tax1.ids)],
'categ_id': self.env.ref('product.product_category_all').id,
})
#add the fiscal position to the PoS
self.main_pos_config.write({
'fiscal_position_ids': [(4, self.fiscal_position.id)],
'tax_regime_selection': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'FiscalPositionNoTaxRefund', login="pos_user")
order = self.env['pos.order'].search([])
self.assertTrue(order[0].name == order[1].name + " REFUND")
def test_lot_refund(self):
self.product1 = self.env['product.product'].create({
'name': 'Product A',
'is_storable': True,
'tracking': 'serial',
'categ_id': self.env.ref('product.product_category_all').id,
'available_in_pos': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'LotRefundTour', login="pos_user")
def test_receipt_tracking_method(self):
self.product_a = self.env['product.product'].create({
'name': 'Product A',
'is_storable': True,
'tracking': 'lot',
'categ_id': self.env.ref('product.product_category_all').id,
'available_in_pos': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ReceiptTrackingMethodTour', login="pos_user")
def test_limited_product_pricelist_loading(self):
self.env['ir.config_parameter'].sudo().set_param('point_of_sale.limited_product_count', '1')
product_1 = self.env['product.product'].create({
'name': 'Test Product 1',
'list_price': 100,
'barcode': '0100100',
'taxes_id': False,
'available_in_pos': True,
})
color_attribute = self.env['product.attribute'].create({
'name': 'Color',
'sequence': 4,
'value_ids': [(0, 0, {
'name': 'White',
'sequence': 1,
}), (0, 0, {
'name': 'Red',
'sequence': 2,
'default_extra_price': 50,
})],
})
product_2_template = self.env['product.template'].create({
'name': 'Test Product 2',
'list_price': 200,
'taxes_id': False,
'available_in_pos': True,
'attribute_line_ids': [(0, 0, {
'attribute_id': color_attribute.id,
'value_ids': [(6, 0, color_attribute.value_ids.ids)]
})],
})
# Check that two product variant are created
self.assertEqual(product_2_template.product_variant_count, 2)
product_2_template.product_variant_ids[0].write({'barcode': '0100201'})
product_2_template.product_variant_ids[1].write({'barcode': '0100202'})
self.env['product.product'].create({
'name': 'Test Product 3',
'list_price': 300,
'barcode': '0100300',
'taxes_id': False,
'available_in_pos': True,
})
pricelist_item = self.env['product.pricelist.item'].create([{
'applied_on': '3_global',
'fixed_price': 50,
}, {
'applied_on': '1_product',
'product_tmpl_id': product_2_template.id,
'fixed_price': 100,
}, {
'applied_on': '0_product_variant',
'product_id': product_1.id,
'fixed_price': 80,
}, {
'applied_on': '0_product_variant',
'product_id': product_2_template.product_variant_ids[1].id,
'fixed_price': 120,
}])
self.main_pos_config.pricelist_id.write({'item_ids': [(6, 0, pricelist_item.ids)]})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'limitedProductPricelistLoading', login="pos_user")
def test_multi_product_pricelist_rules(self):
product_1 = self.env['product.product'].create({
'name': 'Test Product 1',
'list_price': 1,
'taxes_id': False,
'available_in_pos': True,
})
pricelist_item = self.env['product.pricelist.item'].create([{
'applied_on': '3_global',
'fixed_price': 200,
'min_quantity': 0,
}, {
'applied_on': '1_product',
'product_tmpl_id': product_1.product_tmpl_id.id,
'fixed_price': 100,
'min_quantity': 2,
}, {
'applied_on': '0_product_variant',
'product_id': product_1.id,
'fixed_price': 50,
'min_quantity': 3,
}])
self.main_pos_config.pricelist_id.write({'item_ids': [(6, 0, pricelist_item.ids)]})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'multiPricelistRulesTour', login="pos_user")
def test_multi_product_options(self):
self.pos_user.write({
'groups_id': [
(4, self.env.ref('stock.group_stock_manager').id),
]
})
product_a = self.env['product.product'].create({
'name': 'Product A',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
})
chair_multi_attribute = self.env['product.attribute'].create({
'name': 'Multi',
'display_type': 'multi',
'create_variant': 'no_variant',
})
chair_multi_value_1 = self.env['product.attribute.value'].create({
'name': 'Value 1',
'attribute_id': chair_multi_attribute.id,
})
chair_multi_value_2 = self.env['product.attribute.value'].create({
'name': 'Value 2',
'attribute_id': chair_multi_attribute.id,
})
self.chair_multi_line = self.env['product.template.attribute.line'].create({
'product_tmpl_id': product_a.product_tmpl_id.id,
'attribute_id': chair_multi_attribute.id,
'value_ids': [(6, 0, [chair_multi_value_1.id, chair_multi_value_2.id])]
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'MultiProductOptionsTour', login="pos_user")
def test_translate_product_name(self):
self.env['res.lang']._activate_lang('fr_FR')
self.pos_user.write({'lang': 'fr_FR'})
product = self.env['product.product'].create({
'name': 'Test Product',
'list_price': 100,
'taxes_id': False,
'available_in_pos': True,
})
product.update_field_translations('name', {'fr_FR': 'Testez le produit'})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'TranslateProductNameTour', login="pos_user")
def test_properly_display_price(self):
"""Make sure that when the decimal separator is a comma, the shown orderline price is correct.
"""
lang = self.env['res.lang'].search([('code', '=', self.pos_user.lang)])
lang.write({'thousands_sep': '.', 'decimal_point': ','})
self.env['product.product'].create({
'name': 'Test Product',
'list_price': 1_453.53,
'taxes_id': False,
'available_in_pos': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, "DecimalCommaOrderlinePrice", login="pos_user")
def test_res_partner_scan_barcode(self):
# default Customer Barcodes pattern is '042'
self.env['res.partner'].create({
'name': 'John Doe',
'barcode': '0421234567890',
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'BarcodeScanPartnerTour', login="pos_user")
def test_allow_order_modification_after_validation_error(self):
"""
User error as a result of validation should block the order.
Taking action by order modification should be allowed.
"""
self.env['product.product'].create({
'name': 'Test Product',
'list_price': 10.00,
'taxes_id': False,
'available_in_pos': True,
})
def sync_from_ui_patch(*_args, **_kwargs):
raise UserError('Test Error')
with patch.object(self.env.registry.models['pos.order'], "sync_from_ui", sync_from_ui_patch):
# If there is problem in the tour, remove the log catcher to debug.
with self.assertLogs(level="WARNING") as log_catcher:
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'OrderModificationAfterValidationError', login="pos_user")
warning_outputs = [o for o in log_catcher.output if 'WARNING' in o]
self.assertEqual(len(warning_outputs), 1, "Exactly one warning should be logged")
def test_customer_display(self):
self.start_tour(f"/pos_customer_display/{self.main_pos_config.id}/{self.main_pos_config.access_token}", 'CustomerDisplayTour', login="pos_user")
def test_customer_display_with_qr(self):
self.start_tour(f"/pos_customer_display/{self.main_pos_config.id}/{self.main_pos_config.access_token}", 'CustomerDisplayTourWithQr', login="pos_user")
def test_refund_few_quantities(self):
""" Test to check that refund works with quantities of less than 0.5 """
self.env['product.product'].create({
'name': 'Sugar',
'list_price': 3,
'taxes_id': False,
'available_in_pos': True,
'uom_id': self.env.ref('uom.product_uom_kgm').id,
'uom_po_id': self.env.ref('uom.product_uom_kgm').id
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'RefundFewQuantities', login="pos_user")
def test_product_combo_price(self):
""" Check that the combo has the expected price """
self.desk_organizer.write({"lst_price": 7})
self.desk_pad.write({"lst_price": 2.5})
self.whiteboard_pen.write({"lst_price": 1.5})
combos = self.env["product.combo"].create([
{
"name": product.name,
"combo_item_ids": [
Command.create({
"product_id": product.id, "extra_price": 0
})
]
}
for product in (self.desk_organizer, self.desk_pad, self.whiteboard_pen)
])
self.env["product.product"].create(
{
"available_in_pos": True,
"list_price": 7,
"standard_price": 10,
"name": "Desk Combo",
"type": "combo",
"taxes_id": False,
"categ_id": self.env.ref("product.product_category_1").id,
"combo_ids": [
(6, 0, [combo.id for combo in combos])
],
}
)
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", 'ProductComboPriceCheckTour', login="pos_user")
order = self.env['pos.order'].search([], limit=1)
self.assertEqual(order.lines.filtered(lambda l: l.product_id.type == 'combo').margin, 0)
self.assertEqual(order.lines.filtered(lambda l: l.product_id.type == 'combo').margin_percent, 0)
def test_customer_display_as_public(self):
self.main_pos_config.customer_display_type = 'remote'
self.main_pos_config.customer_display_bg_img = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYGAAAAAEAAH2FzhVAAAAAElFTkSuQmCC'
response = self.url_open(f"/web/image/pos.config/{self.main_pos_config.id}/customer_display_bg_img")
self.assertEqual(response.status_code, 200)
self.assertTrue('Shop.png' in response.headers['Content-Disposition'])
def test_customer_all_fields_displayed(self):
"""
Verify that all the field of a partner can be displayed in the partner list.
Also verify that all these fields can be searched.
"""
self.env["res.partner"].create({
"name": "John Doe",
"street": "1 street of astreet",
"city": "Acity",
"state_id": self.env.ref("base.state_us_30").id, # Ohio
"country_id": self.env.ref("base.us").id,
"zip": "26432685463",
"phone": "9898989899",
"mobile": "0987654321",
"email": "john@doe.com"
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_pos_tour('PosCustomerAllFieldsDisplayed')
def test_product_combo_change_fp(self):
"""
Verify than when the fiscal position is changed,
the price of the combo doesn't change and taxes are well taken into account
"""
tax_1 = self.env['account.tax'].create({
'name': 'Tax 10%',
'amount': 10,
'price_include_override': 'tax_included',
'amount_type': 'percent',
'type_tax_use': 'sale',
})
tax_2 = self.env['account.tax'].create({
'name': 'Tax 5%',
'amount': 5,
'price_include_override': 'tax_included',
'amount_type': 'percent',
'type_tax_use': 'sale',
})
setup_product_combo_items(self)
self.office_combo.write({'list_price': 50, 'taxes_id': [(6, 0, [tax_1.id])]})
for combo in self.office_combo.combo_ids: # Set the tax to all the products of the combo
for item in combo.combo_item_ids:
item.product_id.taxes_id = [(6, 0, [tax_1.id])]
fiscal_position = self.env['account.fiscal.position'].create({
'name': 'test fp',
'tax_ids': [(0, 0, {
'tax_src_id': tax_1.id,
'tax_dest_id': tax_2.id,
})],
})
self.main_pos_config.write({
'tax_regime_selection': True,
'fiscal_position_ids': [(6, 0, [fiscal_position.id])],
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", 'ProductComboChangeFP', login="pos_user")
def test_product_combo_change_pricelist(self):
"""
Verify than when we change the pricelist, the combo price is updated
"""
setup_product_combo_items(self)
sale_10_pl = self.env['product.pricelist'].create({
'name': 'sale 10%',
})
self.env['product.pricelist.item'].create({
'pricelist_id': sale_10_pl.id,
'compute_price': 'percentage',
'applied_on': '3_global',
'percent_price': 10,
})
self.main_pos_config.write({
'available_pricelist_ids': [(4, sale_10_pl.id)],
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", 'ProductComboChangePricelist', login="pos_user")
def test_cash_rounding_payment(self):
"""Verify than an error popup is shown if the payment value is more precise than the rounding method"""
rounding_method = self.env['account.cash.rounding'].create({
'name': 'Down 0.10',
'rounding': 0.10,
'strategy': 'add_invoice_line',
'profit_account_id': self.company_data['default_account_revenue'].copy().id,
'loss_account_id': self.company_data['default_account_expense'].copy().id,
'rounding_method': 'DOWN',
})
self.main_pos_config.write({
'cash_rounding': True,
'only_round_cash_method': False,
'rounding_method': rounding_method.id,
})
self.env['ir.config_parameter'].sudo().set_param('barcode.max_time_between_keys_in_ms', 1)
self.main_pos_config.open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'CashRoundingPayment', login="accountman")
def test_product_categories_order(self):
""" Verify that the order of categories doesnt change in the frontend """
self.env['pos.category'].search([]).write({'sequence': 100})
self.env['pos.category'].create({
'name': 'AAA',
'parent_id': False,
'sequence': 1,
})
self.env['pos.category'].create({
'name': 'AAC',
'parent_id': False,
'sequence': 3,
})
parentA = self.env['pos.category'].create({
'name': 'AAB',
'parent_id': False,
'sequence': 2,
})
parentB = self.env['pos.category'].create({
'name': 'AAX',
'parent_id': parentA.id,
})
self.env['pos.category'].create({
'name': 'AAY',
'parent_id': parentB.id,
})
# Add a product that belongs to both parent and child categories.
# It's presence is checked during the tour to make sure app doesn't crash.
self.env['product.product'].create({
'name': 'Product in AAB and AAX',
'pos_categ_ids': [(6, 0, [parentA.id, parentB.id])],
'available_in_pos': True,
})
self.main_pos_config.with_user(self.pos_admin).open_ui()
self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", 'PosCategoriesOrder', login="pos_admin")
def test_autofill_cash_count(self):
"""Make sure that when the decimal separator is a comma, the shown orderline price is correct.
"""
lang = self.env['res.lang'].search([('code', '=', self.pos_user.lang)])
lang.write({'thousands_sep': '.', 'decimal_point': ','})
self.env["product.product"].create(
{
"available_in_pos": True,
"list_price": 123456,
"name": "Test Expensive",
"taxes_id": False
}
)
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, "AutofillCashCount", login="pos_user")
def test_product_search_2(self):
self.env['product.product'].create({
'name': 'Test chair 1',
'available_in_pos': True,
})
self.env['product.product'].create({
'name': 'Test CHAIR 2',
'available_in_pos': True,
})
self.env['product.product'].create({
'name': 'Test sofa',
'available_in_pos': True,
"default_code": "CHAIR_01",
})
self.env['product.product'].create({
'name': 'clémentine',
'available_in_pos': True,
})
self.main_pos_config.open_ui()
self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", 'SearchProducts', login="pos_user")
def test_lot(self):
self.product1 = self.env['product.product'].create({
'name': 'Product A',
'is_storable': True,
'tracking': 'serial',
'categ_id': self.env.ref('product.product_category_all').id,
'available_in_pos': True,
})
product2 = self.env['product.product'].create({
'name': 'Product B',
'is_storable': True,
'tracking': 'lot',
'categ_id': self.env.ref('product.product_category_all').id,
'available_in_pos': True,
})
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product2.id,
'inventory_quantity': 1,
'location_id': self.env.user._get_default_warehouse_id().lot_stock_id.id,
'lot_id': self.env['stock.lot'].create({'name': '1001', 'product_id': product2.id}).id,
}).sudo().action_apply_inventory()
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'LotTour', login="pos_user")
def test_order_with_existing_serial(self):
product = self.env['product.product'].create({
'name': 'Serial Product',
'is_storable': True,
'tracking': 'serial',
'categ_id': self.env.ref('product.product_category_all').id,
'available_in_pos': True,
})
for sn in ["SN1", "SN2"]:
self.env['stock.quant'].create({
'product_id': product.id,
'inventory_quantity': 1,
'location_id': self.env.user._get_default_warehouse_id().lot_stock_id.id,
'lot_id': self.env['stock.lot'].create({'name': sn, 'product_id': product.id}).id,
}).sudo().action_apply_inventory()
self.env['stock.picking.type'].search([('name', '=', 'PoS Orders')]).use_create_lots = False
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_pos_tour("test_order_with_existing_serial")
def test_product_search(self):
"""Verify that the product search works correctly"""
self.env['product.product'].create([
{
'name': 'Test Product 1',
'list_price': 100,
'taxes_id': False,
'available_in_pos': True,
'barcode': '1234567890123',
'default_code': 'TESTPROD1',
},
{
'name': 'Test Product 2',
'list_price': 100,
'taxes_id': False,
'available_in_pos': True,
'barcode': '1234567890124',
'default_code': 'TESTPROD2',
},
{
'name': 'Apple',
'list_price': 100,
'taxes_id': False,
'available_in_pos': True,
},
])
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ProductSearchTour', login="pos_user")
def test_customer_search_more(self):
partner_test_a = self.env["res.partner"].create({"name": "APartner"})
self.env["res.partner"].create({"name": "BPartner", "zip": 1111})
def mocked_get_limited_partners_loading(self):
return [(partner_test_a.id,)]
self.main_pos_config.with_user(self.pos_user).open_ui()
with patch.object(PosConfig, 'get_limited_partners_loading', mocked_get_limited_partners_loading):
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", 'SearchMoreCustomer', login="pos_user")
def test_tracking_number_closing_session(self):
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", 'test_tracking_number_closing_session', login="pos_user")
for order in self.env['pos.order'].search([]):
self.assertEqual(int(order.tracking_number) % 100, 1)
def test_product_card_qty_precision(self):
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", 'ProductCardUoMPrecision', login="pos_user")
def test_reuse_empty_floating_order(self):
""" Verify that after a payment, POS should reuse an existing empty floating order if available, instead of always creating new ones """
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour(f"/pos/ui?config_id={self.main_pos_config.id}", 'test_reuse_empty_floating_order', login="pos_user")
def test_add_multiple_serials_at_once(self):
self.product_a = self.env['product.product'].create({
'name': 'Product A',
'is_storable': True,
'tracking': 'serial',
'categ_id': self.env.ref('product.product_category_all').id,
'available_in_pos': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, "AddMultipleSerialsAtOnce", login="pos_user")
def test_fiscal_position_tax_group_labels(self):
tax_1 = self.env['account.tax'].create({
'name': 'Tax 15%',
'amount': 15,
'price_include_override': 'tax_included',
'amount_type': 'percent',
'type_tax_use': 'sale',
})
tax_1.tax_group_id.pos_receipt_label = 'Tax Group 1'
tax_2 = self.env['account.tax'].create({
'name': 'Tax 5%',
'amount': 5,
'price_include_override': 'tax_included',
'amount_type': 'percent',
'type_tax_use': 'sale',
})
tax_2.tax_group_id.pos_receipt_label = 'Tax Group 2'
self.product = self.env['product.product'].create({
'name': 'Test Product',
'taxes_id': [(6, 0, [tax_1.id])],
'list_price': 100,
'available_in_pos': True,
})
fiscal_position = self.env['account.fiscal.position'].create({
'name': 'Fiscal Position Test',
'tax_ids': [(0, 0, {
'tax_src_id': tax_1.id,
'tax_dest_id': tax_2.id,
})],
})
self.main_pos_config.write({
'tax_regime_selection': True,
'fiscal_position_ids': [(6, 0, [fiscal_position.id])],
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'FiscalPositionTaxLabels', login="pos_user")
def test_order_and_invoice_amounts(self):
payment_term = self.env['account.payment.term'].create({
'name': "early_payment_term",
'discount_percentage': 10,
'discount_days': 10,
'early_discount': True,
'early_pay_discount_computation': 'mixed',
'line_ids': [Command.create({
'value': 'percent',
'nb_days': 0,
'value_amount': 100,
})]
})
self.partner_test_1.property_payment_term_id = payment_term.id
tax = self.env['account.tax'].create({
'name': 'Tax 10%',
'amount': 10,
'amount_type': 'percent',
'type_tax_use': 'sale',
})
self.env['product.product'].create({
'name': 'Product Test',
'available_in_pos': True,
'list_price': 1000,
'taxes_id': [(6, 0, [tax.id])],
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenInvoiceOrder', login="pos_user")
order = self.env['pos.order'].search([('partner_id', '=', self.partner_test_1.id)], limit=1)
self.assertTrue(order)
self.assertEqual(order.partner_id, self.partner_test_1)
invoice = self.env['account.move'].search([('invoice_origin', '=', order.name)], limit=1)
self.assertTrue(invoice)
self.assertFalse(invoice.invoice_payment_term_id)
self.assertAlmostEqual(order.amount_total, invoice.amount_total, places=2, msg="Order and Invoice amounts do not match.")
def test_zero_decimal_places_currency(self):
zero_decimal_currency = self.env['res.currency'].create({
'name': 'ZeroDecimalCurrency',
'symbol': 'ZDC',
'rounding': 1.0,
'decimal_places': 0,
})
self.env.user.company_id.currency_id = zero_decimal_currency
self.main_pos_config.available_pricelist_ids.write({'currency_id': zero_decimal_currency.id})
self.env['product.product'].create({
'name': 'Test Product',
'list_price': 100,
'taxes_id': False,
'available_in_pos': True,
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_zero_decimal_places_currency', login="pos_user")
def test_limited_categories(self):
parent_category = self.env['pos.category'].create({
'name': 'Parent',
})
child_category_1 = self.env['pos.category'].create({
'name': 'Child 1',
'parent_id': parent_category.id,
})
child_category_2 = self.env['pos.category'].create({
'name': 'Child 2',
'parent_id': parent_category.id,
})
self.env['product.product'].create({
'name': 'Product 1',
'available_in_pos': True,
'list_price': 1.20,
'taxes_id': False,
'pos_categ_ids': [(4, child_category_1.id)],
})
self.env['product.product'].create({
'name': 'Product 2',
'available_in_pos': True,
'list_price': 2.30,
'taxes_id': False,
'pos_categ_ids': [(4, child_category_2.id)],
})
self.main_pos_config.write({
'limit_categories': True,
'iface_available_categ_ids': [(6, 0, [parent_category.id, child_category_1.id, child_category_2.id])],
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_limited_categories', login="pos_user")
self.main_pos_config.current_session_id.close_session_from_ui()
# when no category is selected, all products should be displayed
self.main_pos_config.write({
'iface_available_categ_ids': [(6, 0, [])],
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_limited_categories', login="pos_user")
def test_one_attribute_value_scan_barcode(self):
product = self.env['product.template'].create({
'name': 'Product Test',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
'barcode': '1234567',
})
size_attribute = self.env['product.attribute'].create({
'name': 'Size never',
'create_variant': 'no_variant',
'value_ids': [(0, 0, {
'name': 'Large',
})],
})
self.env['product.template.attribute.line'].create({
'product_tmpl_id': product.id,
'attribute_id': size_attribute.id,
'value_ids': [(6, 0, size_attribute.value_ids.ids)]
})
color_attribute = self.env['product.attribute'].create({
'name': 'Color always',
'create_variant': 'always',
'value_ids': [(0, 0, {
'name': 'Red',
'sequence': 1,
}), (0, 0, {
'name': 'Blue',
'sequence': 2,
})],
})
self.env['product.template.attribute.line'].create({
'product_tmpl_id': product.id,
'attribute_id': color_attribute.id,
'value_ids': [(6, 0, color_attribute.value_ids.ids)]
})
product.product_variant_ids[0].barcode = '1234567'
product.product_variant_ids[1].barcode = '1234568'
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_one_attribute_value_scan_barcode', login="pos_user")
def test_draft_orders_not_syncing(self):
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_draft_orders_not_syncing', login="pos_user")
n_draft_order = self.env['pos.order'].search_count([('state', '=', 'draft')], limit=1)
self.assertEqual(n_draft_order, 0, 'There should be no draft orders created')
def test_combo_variant_mix(self):
color_attribute = self.env['product.attribute'].create({
'name': 'Color',
'value_ids': [
Command.create({'name': 'Red'}),
Command.create({'name': 'Blue'})
],
'create_variant': 'no_variant',
})
size_attribute = self.env['product.attribute'].create({
'name': 'Size',
'value_ids': [
Command.create({'name': 'Small'}),
Command.create({'name': 'Large'})
],
'create_variant': 'always',
})
product_template = self.env['product.template'].create({
'name': 'Test Product',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
'attribute_line_ids': [
Command.create({
'attribute_id': color_attribute.id,
'value_ids': [Command.link(id) for id in color_attribute.value_ids.ids]
}),
Command.create({
'attribute_id': size_attribute.id,
'value_ids': [Command.link(id) for id in size_attribute.value_ids.ids]
})
]
})
combo = self.env['product.combo'].create({
'name': 'Test Combo',
'combo_item_ids': [
Command.create({
'product_id': product_template.product_variant_ids[0].id,
'extra_price': 0,
}),
Command.create({
'product_id': product_template.product_variant_ids[1].id,
'extra_price': 0,
}),
]
})
self.env['product.template'].create({
'name': 'Test Product Combo',
'available_in_pos': True,
'list_price': 20,
'taxes_id': False,
'type': 'combo',
'combo_ids': [Command.link(combo.id)],
})
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_combo_variant_mix', login="pos_user")
def test_barcode_search_attributes_preset(self):
product = self.env['product.template'].create({
'name': 'Product with Attributes',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
})
# Product template to force UI reset (acts as a delay)
self.env['product.template'].create({
'name': 'Product without Attributes',
'available_in_pos': True,
'list_price': 20,
'taxes_id': False,
'barcode': '987654321',
})
attribute_1, attribute_2, attribute_3, attribute_4 = self.env['product.attribute'].create([{
'name': 'Attribute 1',
'create_variant': 'always',
'display_type': 'radio',
'value_ids': [(0, 0, {
'name': 'Value 1',
}), (0, 0, {
'name': 'Value 2',
})],
}, {
'name': 'Attribute 2',
'create_variant': 'always',
'display_type': 'pills',
'value_ids': [(0, 0, {
'name': 'Value 3',
}), (0, 0, {
'name': 'Value 4',
})],
}, {
'name': 'Attribute 3',
'create_variant': 'always',
'display_type': 'select',
'value_ids': [(0, 0, {
'name': 'Value 5',
}), (0, 0, {
'name': 'Value 6',
})],
}, {
'name': 'Attribute 4',
'create_variant': 'always',
'display_type': 'color',
'value_ids': [(0, 0, {
'name': 'Value 7',
}), (0, 0, {
'name': 'Value 8',
})],
}])
self.env['product.template.attribute.line'].create([{
'product_tmpl_id': product.id,
'attribute_id': attribute_1.id,
'value_ids': [(6, 0, attribute_1.value_ids.ids)],
'sequence': 1,
}, {
'product_tmpl_id': product.id,
'attribute_id': attribute_2.id,
'value_ids': [(6, 0, attribute_2.value_ids.ids)],
'sequence': 2,
}, {
'product_tmpl_id': product.id,
'attribute_id': attribute_3.id,
'value_ids': [(6, 0, attribute_3.value_ids.ids)],
'sequence': 3,
}, {
'product_tmpl_id': product.id,
'attribute_id': attribute_4.id,
'value_ids': [(6, 0, attribute_4.value_ids.ids)],
'sequence': 4,
}])
for p in product.product_variant_ids:
p.write({
'barcode': f'1234{"".join(p.product_template_attribute_value_ids.mapped(lambda ptav: ptav.name[-1]))}',
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_barcode_search_attributes_preset', login="pos_user")
def test_auto_validate_force_done(self):
self.main_pos_config.write({
'auto_validate_terminal_payment': True
})
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_auto_validate_force_done', login="pos_user")
def test_pos_ui_round_globally(self):
self.main_pos_config.company_id.tax_calculation_rounding_method = 'round_globally'
tax_16 = self.env['account.tax'].create({
'name': 'Tax 16%',
'amount': 16,
})
self.env['product.product'].create([{
'name': 'Test Product 1',
'list_price': 7051.73,
'taxes_id': [(6, 0, [tax_16.id])],
'available_in_pos': True,
}, {
'name': 'Test Product 2',
'list_price': 352.59,
'taxes_id': [(6, 0, [tax_16.id])],
'available_in_pos': True,
}])
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_pos_ui_round_globally', login="pos_user")
pos_session = self.main_pos_config.current_session_id
self.assertEqual(pos_session.order_ids[0].payment_ids[0].amount, 7771.01)
# Close the session and check the session journal entry.
pos_session.action_pos_session_validate()
lines = pos_session.move_id.line_ids.sorted('balance')
self.assertEqual(len(lines), 5, "There should be 5 lines in the session journal entry")
self.assertAlmostEqual(lines[0].balance, -7051.73)
self.assertAlmostEqual(lines[1].balance, -1128.28)
self.assertAlmostEqual(lines[2].balance, 56.41)
self.assertAlmostEqual(lines[3].balance, 352.59)
self.assertAlmostEqual(lines[4].balance, 7771.01)
def test_ctrl_number_ignored(self):
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_ctrl_number_ignored', login="pos_user")
def test_click_all_orders_keep_customer(self):
"""Verify that clicking on 'All Orders' keeps the customer selected."""
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_click_all_orders_keep_customer', login="pos_user")
def test_free_text_custom_attribute_on_receipt(self):
""" Test that free text (custom) attribute values are correctly shown on the PoS receipt screen. """
configurable_product = self.env['product.product'].search([('name', '=', 'Configurable Chair'), ('available_in_pos', '=', 'True')], limit=1)
configurable_product.attribute_line_ids[:2].unlink()
self.main_pos_config.with_user(self.pos_admin).open_ui()
self.start_pos_tour('test_free_text_custom_attribute_on_receipt', login="pos_admin")
def test_sync_from_ui_one_by_one(self):
"""
Sync from UI is now syncing orders one by one.
sync_from_ui should be called 6 times in this tour (6 orders created).
"""
pos_order = self.env.registry.models['pos.order']
sync_counter = {'count': 0}
@api.model
def sync_from_ui_patch(self, orders):
sync_counter['count'] += 1
return super(pos_order, self).sync_from_ui(orders)
with patch.object(pos_order, "sync_from_ui", sync_from_ui_patch):
self.start_pos_tour("test_sync_from_ui_one_by_one", login="pos_user")
self.assertEqual(sync_counter['count'], 6)
# This class just runs the same tests as above but with mobile emulation
class MobileTestUi(TestUi):
browser_size = '375x667'
touch_enabled = True
allow_inherited_tests_method = True
class TestTaxCommonPOS(TestPointOfSaleHttpCommon, TestTaxCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner_a.name = "AAAAAA" # The POS only load the first 100 partners
def create_base_line_product(self, base_line, **kwargs):
return self.env['product.product'].create({
**kwargs,
'available_in_pos': True,
'list_price': base_line['price_unit'],
'taxes_id': [Command.set(base_line['tax_ids'].ids)],
'pos_categ_ids': [Command.set(self.pos_desk_misc_test.ids)],
})
def ensure_products_on_document(self, document, product_prefix):
for i, base_line in enumerate(document['lines'], start=1):
base_line['product_id'] = self.create_base_line_product(base_line, name=f'{product_prefix}_{i}')
def assert_pos_order_totals(self, order, expected_values):
expected_amounts = {}
if 'tax_amount_currency' in expected_values:
expected_amounts['amount_tax'] = expected_values['tax_amount_currency']
if 'total_amount_currency' in expected_values:
expected_amounts['amount_total'] = expected_values['total_amount_currency']
self.assertRecordValues(order, [expected_amounts])