# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import time from freezegun import freeze_time from datetime import datetime import odoo from odoo import fields, tools from odoo.tools import float_compare, mute_logger, test_reports from odoo.tests.common import Form from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon @odoo.tests.tagged('post_install', '-at_install') class TestPointOfSaleFlow(TestPointOfSaleCommon): def compute_tax(self, product, price, qty=1, taxes=None): if not taxes: taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id) currency = self.pos_config.currency_id res = taxes.compute_all(price, currency, qty, product=product) untax = res['total_excluded'] return untax, sum(tax.get('amount', 0.0) for tax in res['taxes']) def _create_pos_order_for_postponed_invoicing(self): # Create the order on the first of january. with freeze_time('2020-01-01'): product = self.env['product.product'].create({ 'name': 'Dummy product', 'type': 'product', 'categ_id': self.env.ref('product.product_category_all').id, 'taxes_id': self.tax_sale_a.ids, }) self.pos_config.open_ui() pos_session = self.pos_config.current_session_id untax, atax = self.compute_tax(product, 500, 1) pos_order_data = { 'data': { 'amount_paid': untax + atax, 'amount_return': 0, 'amount_tax': atax, 'amount_total': untax + atax, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, 'lines': [(0, 0, { 'discount': 0, 'id': 42, 'pack_lot_ids': [], 'price_unit': 500.0, 'product_id': product.id, 'price_subtotal': 500.0, 'price_subtotal_incl': 575.0, 'qty': 1, 'tax_ids': [(6, 0, product.taxes_id.ids)] })], 'name': 'Order 12345-123-1234', 'partner_id': False, 'pos_session_id': pos_session.id, 'sequence_number': 2, 'statement_ids': [(0, 0, { 'amount': untax + atax, 'name': fields.Datetime.now(), 'payment_method_id': self.cash_payment_method.id })], 'uid': '12345-123-1234', 'user_id': self.env.uid }, 'id': '12345-123-1234', 'to_invoice': False } pos_order_id = self.PosOrder.create_from_ui([pos_order_data])[0]['id'] pos_order = self.env['pos.order'].browse(pos_order_id) # End the session. The order has been created without any invoice. self.pos_config.current_session_id.action_pos_session_closing_control() return pos_order def test_order_refund(self): self.pos_config.open_ui() current_session = self.pos_config.current_session_id # I create a new PoS order with 2 lines order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product3.id, 'price_unit': 450, 'discount': 5.0, 'qty': 2.0, 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': 450 * (1 - 5/100.0) * 2, 'price_subtotal_incl': 450 * (1 - 5/100.0) * 2, }), (0, 0, { 'name': "OL/0002", 'product_id': self.product4.id, 'price_unit': 300, 'discount': 5.0, 'qty': 3.0, 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': 300 * (1 - 5/100.0) * 3, 'price_subtotal_incl': 300 * (1 - 5/100.0) * 3, })], 'amount_total': 1710.0, 'amount_tax': 0.0, 'amount_paid': 0.0, 'amount_return': 0.0, 'last_order_preparation_change': '{}' }) payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() self.assertAlmostEqual(order.amount_total, order.amount_paid, msg='Order should be fully paid.') # I create a refund refund_action = order.refund() refund = self.PosOrder.browse(refund_action['res_id']) self.assertEqual(order.amount_total, -1*refund.amount_total, "The refund does not cancel the order (%s and %s)" % (order.amount_total, refund.amount_total)) payment_context = {"active_ids": refund.ids, "active_id": refund.id} refund_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': refund.amount_total, 'payment_method_id': self.cash_payment_method.id, }) # I click on the validate button to register the payment. refund_payment.with_context(**payment_context).check() self.assertEqual(refund.state, 'paid', "The refund is not marked as paid") self.assertTrue(refund.payment_ids.payment_method_id.is_cash_count, msg='There should only be one payment and paid in cash.') total_cash_payment = sum(current_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) current_session.post_closing_cash_details(total_cash_payment) current_session.close_session_from_ui() self.assertEqual(current_session.state, 'closed', msg='State of current session should be closed.') def test_order_refund_lots(self): # open pos session self.pos_config.open_ui() current_session = self.pos_config.current_session_id # set up product iwith SN tracing and create two lots (1001, 1002) self.stock_location = self.company_data['default_warehouse'].lot_stock_id self.product2 = self.env['product.product'].create({ 'name': 'Product A', 'type': 'product', 'tracking': 'serial', 'categ_id': self.env.ref('product.product_category_all').id, }) lot1 = self.env['stock.lot'].create({ 'name': '1001', 'product_id': self.product2.id, 'company_id': self.env.company.id, }) lot2 = self.env['stock.lot'].create({ 'name': '1002', 'product_id': self.product2.id, 'company_id': self.env.company.id, }) self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': self.product2.id, 'inventory_quantity': 1, 'location_id': self.stock_location.id, 'lot_id': lot1.id }).action_apply_inventory() self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': self.product2.id, 'inventory_quantity': 1, 'location_id': self.stock_location.id, 'lot_id': lot2.id }).action_apply_inventory() # create pos order with the two SN created before order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product2.id, 'price_unit': 6, 'discount': 0, 'qty': 2, 'tax_ids': [[6, False, []]], 'price_subtotal': 12, 'price_subtotal_incl': 12, 'pack_lot_ids': [ [0, 0, {'lot_name': '1001'}], [0, 0, {'lot_name': '1002'}], ] })], 'pricelist_id': self.pos_config.pricelist_id.id, 'amount_paid': 12.0, 'amount_total': 12.0, 'amount_tax': 0.0, 'amount_return': 0.0, 'to_invoice': False, 'last_order_preparation_change': '{}' }) payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() # I create a refund refund_action = order.refund() refund = self.PosOrder.browse(refund_action['res_id']) order_lot_id = [lot_id.lot_name for lot_id in order.lines.pack_lot_ids] refund_lot_id = [lot_id.lot_name for lot_id in refund.lines.pack_lot_ids] self.assertEqual( order_lot_id, refund_lot_id, "In the refund we should find the same lot as in the original order") payment_context = {"active_ids": refund.ids, "active_id": refund.id} refund_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': refund.amount_total, 'payment_method_id': self.cash_payment_method.id, }) # I click on the validate button to register the payment. refund_payment.with_context(**payment_context).check() self.assertEqual(refund.state, 'paid', "The refund is not marked as paid") current_session.action_pos_session_closing_control() def test_order_to_picking(self): """ In order to test the Point of Sale in module, I will do three orders from the sale to the payment, invoicing + picking, but will only check the picking consistency in the end. TODO: Check the negative picking after changing the picking relation to One2many (also for a mixed use case), check the quantity, the locations and return picking logic """ # I click on create a new session button self.pos_config.open_ui() current_session = self.pos_config.current_session_id # I create a PoS order with 2 units of PCSC234 at 450 EUR # and 3 units of PCSC349 at 300 EUR. untax1, atax1 = self.compute_tax(self.product3, 450, 2) untax2, atax2 = self.compute_tax(self.product4, 300, 3) self.pos_order_pos1 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product3.id, 'price_unit': 450, 'discount': 0.0, 'qty': 2.0, 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax1, 'price_subtotal_incl': untax1 + atax1, }), (0, 0, { 'name': "OL/0002", 'product_id': self.product4.id, 'price_unit': 300, 'discount': 0.0, 'qty': 3.0, 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax2, 'price_subtotal_incl': untax2 + atax2, })], 'amount_tax': atax1 + atax2, 'amount_total': untax1 + untax2 + atax1 + atax2, 'amount_paid': 0, 'amount_return': 0, 'last_order_preparation_change': '{}' }) context_make_payment = { "active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id } self.pos_make_payment_2 = self.PosMakePayment.with_context(context_make_payment).create({ 'amount': untax1 + untax2 + atax1 + atax2 }) # I click on the validate button to register the payment. context_payment = {'active_id': self.pos_order_pos1.id} self.pos_make_payment_2.with_context(context_payment).check() # I check that the order is marked as paid self.assertEqual( self.pos_order_pos1.state, 'paid', 'Order should be in paid state.' ) # I test that the pickings are created as expected during payment # One picking attached and having all the positive move lines in the correct state self.assertEqual( self.pos_order_pos1.picking_ids[0].state, 'done', 'Picking should be in done state.' ) self.assertEqual( self.pos_order_pos1.picking_ids[0].move_ids.mapped('state'), ['done', 'done'], 'Move Lines should be in done state.' ) # I create a second order untax1, atax1 = self.compute_tax(self.product3, 450, -2) untax2, atax2 = self.compute_tax(self.product4, 300, -3) self.pos_order_pos2 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0003", 'product_id': self.product3.id, 'price_unit': 450, 'discount': 0.0, 'qty': (-2.0), 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax1, 'price_subtotal_incl': untax1 + atax1, }), (0, 0, { 'name': "OL/0004", 'product_id': self.product4.id, 'price_unit': 300, 'discount': 0.0, 'qty': (-3.0), 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax2, 'price_subtotal_incl': untax2 + atax2, })], 'amount_tax': atax1 + atax2, 'amount_total': untax1 + untax2 + atax1 + atax2, 'amount_paid': 0, 'amount_return': 0, 'last_order_preparation_change': '{}' }) context_make_payment = { "active_ids": [self.pos_order_pos2.id], "active_id": self.pos_order_pos2.id } self.pos_make_payment_3 = self.PosMakePayment.with_context(context_make_payment).create({ 'amount': untax1 + untax2 + atax1 + atax2 }) # I click on the validate button to register the payment. context_payment = {'active_id': self.pos_order_pos2.id} self.pos_make_payment_3.with_context(context_payment).check() # I check that the order is marked as paid self.assertEqual( self.pos_order_pos2.state, 'paid', 'Order should be in paid state.' ) # I test that the pickings are created as expected # One picking attached and having all the positive move lines in the correct state self.assertEqual( self.pos_order_pos2.picking_ids[0].state, 'done', 'Picking should be in done state.' ) self.assertEqual( self.pos_order_pos2.picking_ids[0].move_ids.mapped('state'), ['done', 'done'], 'Move Lines should be in done state.' ) untax1, atax1 = self.compute_tax(self.product3, 450, -2) untax2, atax2 = self.compute_tax(self.product4, 300, 3) self.pos_order_pos3 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0005", 'product_id': self.product3.id, 'price_unit': 450, 'discount': 0.0, 'qty': (-2.0), 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax1, 'price_subtotal_incl': untax1 + atax1, }), (0, 0, { 'name': "OL/0006", 'product_id': self.product4.id, 'price_unit': 300, 'discount': 0.0, 'qty': 3.0, 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax2, 'price_subtotal_incl': untax2 + atax2, })], 'amount_tax': atax1 + atax2, 'amount_total': untax1 + untax2 + atax1 + atax2, 'amount_paid': 0, 'amount_return': 0, 'last_order_preparation_change': '{}' }) context_make_payment = { "active_ids": [self.pos_order_pos3.id], "active_id": self.pos_order_pos3.id } self.pos_make_payment_4 = self.PosMakePayment.with_context(context_make_payment).create({ 'amount': untax1 + untax2 + atax1 + atax2, }) # I click on the validate button to register the payment. context_payment = {'active_id': self.pos_order_pos3.id} self.pos_make_payment_4.with_context(context_payment).check() # I check that the order is marked as paid self.assertEqual( self.pos_order_pos3.state, 'paid', 'Order should be in paid state.' ) # I test that the pickings are created as expected # One picking attached and having all the positive move lines in the correct state self.assertEqual( self.pos_order_pos3.picking_ids[0].state, 'done', 'Picking should be in done state.' ) self.assertEqual( self.pos_order_pos3.picking_ids[0].move_ids.mapped('state'), ['done'], 'Move Lines should be in done state.' ) # I close the session to generate the journal entries self.pos_config.current_session_id.action_pos_session_closing_control() def test_order_to_picking02(self): """ This test is similar to test_order_to_picking except that this time, there are two products: - One tracked by lot - One untracked - Both are in a sublocation of the main warehouse """ tracked_product, untracked_product = self.env['product.product'].create([{ 'name': 'SuperProduct Tracked', 'type': 'product', 'tracking': 'lot', 'available_in_pos': True, }, { 'name': 'SuperProduct Untracked', 'type': 'product', 'available_in_pos': True, }]) wh_location = self.company_data['default_warehouse'].lot_stock_id shelf1_location = self.env['stock.location'].create({ 'name': 'shelf1', 'usage': 'internal', 'location_id': wh_location.id, }) lot = self.env['stock.lot'].create({ 'name': 'SuperLot', 'product_id': tracked_product.id, 'company_id': self.env.company.id, }) qty = 2 self.env['stock.quant']._update_available_quantity(tracked_product, shelf1_location, qty, lot_id=lot) self.env['stock.quant']._update_available_quantity(untracked_product, shelf1_location, qty) self.pos_config.open_ui() self.pos_config.current_session_id.update_stock_at_closing = False untax, atax = self.compute_tax(tracked_product, 1.15, 1) for dummy in range(qty): pos_order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': self.pos_config.current_session_id.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': tracked_product.id, 'price_unit': 1.15, 'discount': 0.0, 'qty': 1.0, 'tax_ids': [(6, 0, tracked_product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax, 'price_subtotal_incl': untax + atax, 'pack_lot_ids': [[0, 0, {'lot_name': lot.name}]], }), (0, 0, { 'name': "OL/0002", 'product_id': untracked_product.id, 'price_unit': 1.15, 'discount': 0.0, 'qty': 1.0, 'tax_ids': [(6, 0, untracked_product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax, 'price_subtotal_incl': untax + atax, })], 'amount_tax': 2 * atax, 'amount_total': 2 * (untax + atax), 'amount_paid': 0, 'amount_return': 0, 'last_order_preparation_change': '{}' }) context_make_payment = { "active_ids": [pos_order.id], "active_id": pos_order.id, } pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({ 'amount': 2 * (untax + atax), }) context_payment = {'active_id': pos_order.id} pos_make_payment.with_context(context_payment).check() self.assertEqual(pos_order.state, 'paid') tracked_line = pos_order.picking_ids.move_line_ids.filtered(lambda ml: ml.product_id.id == tracked_product.id) untracked_line = pos_order.picking_ids.move_line_ids - tracked_line self.assertEqual(tracked_line.lot_id, lot) self.assertFalse(untracked_line.lot_id) self.assertEqual(tracked_line.location_id, shelf1_location) self.assertEqual(untracked_line.location_id, shelf1_location) self.pos_config.current_session_id.action_pos_session_closing_control() def test_order_to_invoice(self): self.pos_config.open_ui() current_session = self.pos_config.current_session_id untax1, atax1 = self.compute_tax(self.product3, 450*0.95, 2) untax2, atax2 = self.compute_tax(self.product4, 300*0.95, 3) # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) self.pos_order_pos1 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product3.id, 'price_unit': 450, 'discount': 5.0, 'qty': 2.0, 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax1, 'price_subtotal_incl': untax1 + atax1, }), (0, 0, { 'name': "OL/0002", 'product_id': self.product4.id, 'price_unit': 300, 'discount': 5.0, 'qty': 3.0, 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)], 'price_subtotal': untax2, 'price_subtotal_incl': untax2 + atax2, })], 'amount_tax': atax1 + atax2, 'amount_total': untax1 + untax2 + atax1 + atax2, 'amount_paid': 0.0, 'amount_return': 0.0, 'last_order_preparation_change': '{}' }) # I click on the "Make Payment" wizard to pay the PoS order context_make_payment = {"active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id} self.pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({ 'amount': untax1 + untax2 + atax1 + atax2, }) # I click on the validate button to register the payment. context_payment = {'active_id': self.pos_order_pos1.id} self.pos_make_payment.with_context(context_payment).check() # I check that the order is marked as paid and there is no invoice # attached to it self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.") self.assertFalse(self.pos_order_pos1.account_move, 'Invoice should not be attached to order.') # I generate an invoice from the order res = self.pos_order_pos1.action_pos_order_invoice() self.assertIn('res_id', res, "Invoice should be created") # I test that the total of the attached invoice is correct invoice = self.env['account.move'].browse(res['res_id']) if invoice.state != 'posted': invoice.action_post() self.assertAlmostEqual( invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct") # I close the session to generate the journal entries current_session.action_pos_session_closing_control() def test_create_from_ui(self): """ Simulation of sales coming from the interface, even after closing the session """ # I click on create a new session button self.pos_config.open_ui() current_session = self.pos_config.current_session_id num_starting_orders = len(current_session.order_ids) current_session.set_cashbox_pos(0, None) untax, atax = self.compute_tax(self.led_lamp, 0.9) carrot_order = {'data': {'amount_paid': untax + atax, 'amount_return': 0, 'amount_tax': atax, 'amount_total': untax + atax, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, 'lines': [[0, 0, {'discount': 0, 'pack_lot_ids': [], 'price_unit': 0.9, 'product_id': self.led_lamp.id, 'price_subtotal': 0.9, 'price_subtotal_incl': 1.04, 'qty': 1, 'tax_ids': [(6, 0, self.led_lamp.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)]}]], 'name': 'Order 00042-003-0014', 'partner_id': False, 'pos_session_id': current_session.id, 'sequence_number': 2, 'statement_ids': [[0, 0, {'amount': untax + atax, 'name': fields.Datetime.now(), 'payment_method_id': self.cash_payment_method.id}]], 'uid': '00042-003-0014', 'user_id': self.env.uid}, 'to_invoice': False} untax, atax = self.compute_tax(self.whiteboard_pen, 1.2) zucchini_order = {'data': {'amount_paid': untax + atax, 'amount_return': 0, 'amount_tax': atax, 'amount_total': untax + atax, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, 'lines': [[0, 0, {'discount': 0, 'pack_lot_ids': [], 'price_unit': 1.2, 'product_id': self.whiteboard_pen.id, 'price_subtotal': 1.2, 'price_subtotal_incl': 1.38, 'qty': 1, 'tax_ids': [(6, 0, self.whiteboard_pen.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)]}]], 'name': 'Order 00043-003-0014', 'partner_id': self.partner1.id, 'pos_session_id': current_session.id, 'sequence_number': self.pos_config.journal_id.id, 'statement_ids': [[0, 0, {'amount': untax + atax, 'name': fields.Datetime.now(), 'payment_method_id': self.credit_payment_method.id}]], 'uid': '00043-003-0014', 'user_id': self.env.uid}, 'to_invoice': False} untax, atax = self.compute_tax(self.newspaper_rack, 1.28) newspaper_rack_order = {'data': {'amount_paid': untax + atax, 'amount_return': 0, 'amount_tax': atax, 'amount_total': untax + atax, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, 'lines': [[0, 0, {'discount': 0, 'pack_lot_ids': [], 'price_unit': 1.28, 'product_id': self.newspaper_rack.id, 'price_subtotal': 1.28, 'price_subtotal_incl': 1.47, 'qty': 1, 'tax_ids': [[6, False, self.newspaper_rack.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids]]}]], 'name': 'Order 00044-003-0014', 'partner_id': False, 'pos_session_id': current_session.id, 'sequence_number': self.pos_config.journal_id.id, 'statement_ids': [[0, 0, {'amount': untax + atax, 'name': fields.Datetime.now(), 'payment_method_id': self.bank_payment_method.id}]], 'uid': '00044-003-0014', 'user_id': self.env.uid}, 'to_invoice': False} # I create an order on an open session self.PosOrder.create_from_ui([carrot_order]) self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Submitted order not encoded") # I close the session total_cash_payment = sum(current_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) current_session.post_closing_cash_details(total_cash_payment) current_session.close_session_from_ui() self.assertEqual(current_session.state, 'closed', "Session was not properly closed") self.assertFalse(self.pos_config.current_session_id, "Current session not properly recomputed") # I keep selling after the session is closed with mute_logger('odoo.addons.point_of_sale.models.pos_order'): self.PosOrder.create_from_ui([zucchini_order, newspaper_rack_order]) rescue_session = self.PosSession.search([ ('config_id', '=', self.pos_config.id), ('state', '=', 'opened'), ('rescue', '=', True) ]) self.assertEqual(len(rescue_session), 1, "One (and only one) rescue session should be created for orphan orders") self.assertIn("(RESCUE FOR %s)" % current_session.name, rescue_session.name, "Rescue session is not linked to the previous one") self.assertEqual(len(rescue_session.order_ids), 2, "Rescue session does not contain both orders") # I close the rescue session total_cash_payment = sum(rescue_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) rescue_session.post_closing_cash_details(total_cash_payment) rescue_session.close_session_from_ui() self.assertEqual(rescue_session.state, 'closed', "Rescue session was not properly closed") self.assertEqual(rescue_session.cash_register_balance_start, current_session.cash_register_balance_end_real, "Rescue session does not start with the same amount as the previous session") def test_order_to_payment_currency(self): """ In order to test the Point of Sale in module, I will do a full flow from the sale to the payment and invoicing. I will use two products, one with price including a 10% tax, the other one with 5% tax excluded from the price. The order will be in a different currency than the company currency. """ # Make sure the company is in USD self.env.ref('base.USD').active = True self.env.ref('base.EUR').active = True self.env.cr.execute( "UPDATE res_company SET currency_id = %s WHERE id = %s", [self.env.ref('base.USD').id, self.env.company.id]) # Demo data are crappy, clean-up the rates self.env['res.currency.rate'].search([]).unlink() self.env['res.currency.rate'].create({ 'name': '2010-01-01', 'rate': 2.0, 'currency_id': self.env.ref('base.EUR').id, }) # make a config that has currency different from the company eur_pricelist = self.env['product.pricelist'].create({'name': 'Test EUR Pricelist', 'currency_id': self.env.ref('base.EUR').id}) sale_journal = self.env['account.journal'].create({ 'name': 'PoS Sale EUR', 'type': 'sale', 'code': 'POSE', 'company_id': self.company.id, 'sequence': 12, 'currency_id': self.env.ref('base.EUR').id }) eur_config = self.pos_config.create({ 'name': 'Shop EUR Test', 'journal_id': sale_journal.id, 'use_pricelist': True, 'available_pricelist_ids': [(6, 0, eur_pricelist.ids)], 'pricelist_id': eur_pricelist.id, 'payment_method_ids': [(6, 0, self.bank_payment_method.ids)] }) # I click on create a new session button eur_config.open_ui() current_session = eur_config.current_session_id # I create a PoS order with 2 units of PCSC234 at 450 EUR (Tax Incl) # and 3 units of PCSC349 at 300 EUR. (Tax Excl) untax1, atax1 = self.compute_tax(self.product3, 450, 2) untax2, atax2 = self.compute_tax(self.product4, 300, 3) self.pos_order_pos0 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'pricelist_id': eur_pricelist.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product3.id, 'price_unit': 450, 'discount': 0.0, 'qty': 2.0, 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id == self.env.company).ids)], 'price_subtotal': untax1, 'price_subtotal_incl': untax1 + atax1, }), (0, 0, { 'name': "OL/0002", 'product_id': self.product4.id, 'price_unit': 300, 'discount': 0.0, 'qty': 3.0, 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id == self.env.company).ids)], 'price_subtotal': untax2, 'price_subtotal_incl': untax2 + atax2, })], 'amount_tax': atax1 + atax2, 'amount_total': untax1 + untax2 + atax1 + atax2, 'amount_paid': 0.0, 'amount_return': 0.0, 'last_order_preparation_change': '{}' }) # I check that the total of the order is now equal to (450*2 + # 300*3*1.05)*0.95 self.assertLess( abs(self.pos_order_pos0.amount_total - (450 * 2 + 300 * 3 * 1.05)), 0.01, 'The order has a wrong total including tax and discounts') # I click on the "Make Payment" wizard to pay the PoS order with a # partial amount of 100.0 EUR context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({ 'amount': 100.0, 'payment_method_id': self.bank_payment_method.id, }) # I click on the validate button to register the payment. context_payment = {'active_id': self.pos_order_pos0.id} self.pos_make_payment_0.with_context(context_payment).check() # I check that the order is not marked as paid yet self.assertEqual(self.pos_order_pos0.state, 'draft', 'Order should be in draft state.') # On the second payment proposition, I check that it proposes me the # remaining balance which is 1790.0 EUR defs = self.pos_make_payment_0.with_context({'active_id': self.pos_order_pos0.id}).default_get(['amount']) self.assertLess( abs(defs['amount'] - ((450 * 2 + 300 * 3 * 1.05) - 100.0)), 0.01, "The remaining balance is incorrect.") #'I pay the remaining balance. context_make_payment = { "active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} self.pos_make_payment_1 = self.PosMakePayment.with_context(context_make_payment).create({ 'amount': (450 * 2 + 300 * 3 * 1.05) - 100.0, 'payment_method_id': self.bank_payment_method.id, }) # I click on the validate button to register the payment. self.pos_make_payment_1.with_context(context_make_payment).check() # I check that the order is marked as paid self.assertEqual(self.pos_order_pos0.state, 'paid', 'Order should be in paid state.') # I generate the journal entries current_session.action_pos_session_validate() # I test that the generated journal entry is attached to the PoS order self.assertTrue(current_session.move_id, "Journal entry should have been attached to the session.") # Check the amounts debit_lines = current_session.move_id.mapped('line_ids.debit') credit_lines = current_session.move_id.mapped('line_ids.credit') amount_currency_lines = current_session.move_id.mapped('line_ids.amount_currency') for a, b in zip(sorted(debit_lines), [0.0, 0.0, 0.0, 0.0, 922.5]): self.assertAlmostEqual(a, b) for a, b in zip(sorted(credit_lines), [0.0, 22.5, 40.91, 409.09, 450]): self.assertAlmostEqual(a, b) for a, b in zip(sorted(amount_currency_lines), [-900, -818.18, -81.82, -45, 1845]): self.assertAlmostEqual(a, b) def test_order_to_invoice_no_tax(self): self.pos_config.open_ui() current_session = self.pos_config.current_session_id # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) self.pos_order_pos1 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product3.id, 'price_unit': 450, 'discount': 5.0, 'qty': 2.0, 'price_subtotal': 855, 'price_subtotal_incl': 855, }), (0, 0, { 'name': "OL/0002", 'product_id': self.product4.id, 'price_unit': 300, 'discount': 5.0, 'qty': 3.0, 'price_subtotal': 855, 'price_subtotal_incl': 855, })], 'amount_tax': 855 * 2, 'amount_total': 855 * 2, 'amount_paid': 0.0, 'amount_return': 0.0, 'last_order_preparation_change': '{}' }) # I click on the "Make Payment" wizard to pay the PoS order context_make_payment = {"active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id} self.pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({ 'amount': 855 * 2, }) # I click on the validate button to register the payment. context_payment = {'active_id': self.pos_order_pos1.id} self.pos_make_payment.with_context(context_payment).check() # I check that the order is marked as paid and there is no invoice # attached to it self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.") self.assertFalse(self.pos_order_pos1.account_move, 'Invoice should not be attached to order yet.') # I generate an invoice from the order res = self.pos_order_pos1.action_pos_order_invoice() self.assertIn('res_id', res, "No invoice created") # I test that the total of the attached invoice is correct invoice = self.env['account.move'].browse(res['res_id']) if invoice.state != 'posted': invoice.action_post() self.assertAlmostEqual( invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct") for iline in invoice.invoice_line_ids: self.assertFalse(iline.tax_ids) self.pos_config.current_session_id.action_pos_session_closing_control() def test_order_with_deleted_tax(self): # create tax dummy_50_perc_tax = self.env['account.tax'].create({ 'name': 'Tax 50%', 'amount_type': 'percent', 'amount': 50.0, 'price_include': 0 }) # set tax to product product5 = self.env['product.product'].create({ 'name': 'product5', 'type': 'product', 'categ_id': self.env.ref('product.product_category_all').id, 'taxes_id': dummy_50_perc_tax.ids }) # sell product thru pos self.pos_config.open_ui() pos_session = self.pos_config.current_session_id untax, atax = self.compute_tax(product5, 10.0) product5_order = {'data': {'amount_paid': untax + atax, 'amount_return': 0, 'amount_tax': atax, 'amount_total': untax + atax, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, 'lines': [[0, 0, {'discount': 0, 'pack_lot_ids': [], 'price_unit': 10.0, 'product_id': product5.id, 'price_subtotal': 10.0, 'price_subtotal_incl': 15.0, 'qty': 1, 'tax_ids': [(6, 0, product5.taxes_id.ids)]}]], 'name': 'Order 12345-123-1234', 'partner_id': False, 'pos_session_id': pos_session.id, 'sequence_number': 2, 'statement_ids': [[0, 0, {'amount': untax + atax, 'name': fields.Datetime.now(), 'payment_method_id': self.cash_payment_method.id}]], 'uid': '12345-123-1234', 'user_id': self.env.uid}, 'to_invoice': False} self.PosOrder.create_from_ui([product5_order]) # delete tax dummy_50_perc_tax.unlink() total_cash_payment = sum(pos_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount')) pos_session.post_closing_cash_details(total_cash_payment) # close session (should not fail here) # We don't call `action_pos_session_closing_control` to force the failed # closing which will return the action because the internal rollback call messes # with the rollback of the test runner. So instead, we directly call the method # that returns the action by specifying the imbalance amount. action = pos_session._close_session_action(5.0) wizard = self.env['pos.close.session.wizard'].browse(action['res_id']) wizard.with_context(action['context']).close_session() # check the difference line diff_line = pos_session.move_id.line_ids.filtered(lambda line: line.name == 'Difference at closing PoS session') self.assertAlmostEqual(diff_line.credit, 5.0, msg="Missing amount of 5.0") def test_order_multi_step_route(self): """ Test that orders in sessions with "Ship Later" enabled and "Specific Route" set to a multi-step (2/3) route can be validated. This config implies multiple picking types and multiple move_lines. """ tracked_product = self.env['product.product'].create({ 'name': 'SuperProduct Tracked', 'type': 'product', 'tracking': 'lot', 'available_in_pos': True }) tracked_product_2 = self.env['product.product'].create({ 'name': 'SuperProduct Tracked 2', 'type': 'product', 'tracking': 'lot', 'available_in_pos': True }) tracked_product_2_lot = self.env['stock.lot'].create({ 'name': '80085', 'product_id': tracked_product_2.id, 'company_id': self.env.company.id, }) stock_location = self.company_data['default_warehouse'].lot_stock_id self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': tracked_product_2.id, 'inventory_quantity': 1, 'location_id': stock_location.id, 'lot_id': tracked_product_2_lot.id }).action_apply_inventory() warehouse_id = self.company_data['default_warehouse'] warehouse_id.delivery_steps = 'pick_ship' self.pos_config.ship_later = True self.pos_config.warehouse_id = warehouse_id self.pos_config.route_id = warehouse_id.route_ids[-1] self.pos_config.open_ui() self.pos_config.current_session_id.update_stock_at_closing = False untax, tax = self.compute_tax(tracked_product, 1.15, 1) pos_order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': self.pos_config.current_session_id.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': tracked_product.id, 'price_unit': 1.15, 'qty': 1.0, 'price_subtotal': untax, 'price_subtotal_incl': untax + tax, 'pack_lot_ids': [ [0, 0, {'lot_name': '80085'}], ] }), (0, 0, { 'name': "OL/0002", 'product_id': tracked_product_2.id, 'price_unit': 1.15, 'qty': 1.0, 'price_subtotal': untax, 'price_subtotal_incl': untax + tax, 'pack_lot_ids': [ [0, 0, {'lot_name': '80085'}], ] })], 'amount_tax': tax, 'amount_total': untax+tax, 'amount_paid': 0, 'amount_return': 0, 'shipping_date': fields.Date.today(), 'last_order_preparation_change': '{}' }) context_make_payment = { "active_ids": [pos_order.id], "active_id": pos_order.id, } pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({ 'amount': untax+tax, }) context_payment = {'active_id': pos_order.id} pos_make_payment.with_context(context_payment).check() pickings = pos_order.picking_ids picking_mls_no_stock = pickings.move_line_ids.filtered(lambda l: l.product_id.id == tracked_product.id) picking_mls_stock = pickings.move_line_ids.filtered(lambda l: l.product_id.id == tracked_product_2.id) self.assertEqual(pos_order.state, 'paid') self.assertEqual(len(picking_mls_no_stock), 0) self.assertEqual(len(picking_mls_stock), 1) self.assertEqual(len(pickings.picking_type_id), 2) def test_order_refund_picking(self): self.pos_config.open_ui() current_session = self.pos_config.current_session_id current_session.update_stock_at_closing = True # I create a new PoS order with 1 line order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product3.id, 'price_unit': 450, 'discount': 5.0, 'qty': 2.0, 'tax_ids': [(6, 0, self.product3.taxes_id.ids)], 'price_subtotal': 450 * (1 - 5/100.0) * 2, 'price_subtotal_incl': 450 * (1 - 5/100.0) * 2, })], 'amount_total': 1710.0, 'amount_tax': 0.0, 'amount_paid': 0.0, 'amount_return': 0.0, 'to_invoice': True, 'last_order_preparation_change': '{}' }) payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() # I create a refund refund_action = order.refund() refund = self.PosOrder.browse(refund_action['res_id']) payment_context = {"active_ids": refund.ids, "active_id": refund.id} refund_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': refund.amount_total, 'payment_method_id': self.cash_payment_method.id, }) # I click on the validate button to register the payment. refund_payment.with_context(**payment_context).check() refund.action_pos_order_invoice() self.assertEqual(refund.picking_count, 1) def test_order_with_different_payments_and_refund(self): """ Test that all the payments are correctly taken into account when the order contains multiple payments and money refund. In this example, we create an order with two payments for a product of 750$: - one payment of $300 with customer account - one payment of $460 with cash Then, we refund the order with $10, and check that the amount still due is 300$. """ product5 = self.env['product.product'].create({ 'name': 'product5', 'type': 'product', 'categ_id': self.env.ref('product.product_category_all').id, }) # sell product thru pos self.pos_config.open_ui() pos_session = self.pos_config.current_session_id product5_order = {'data': {'amount_paid': 750, 'amount_return': 10, 'amount_tax': 0, 'amount_total': 750, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, 'lines': [[0, 0, { 'discount': 0, 'pack_lot_ids': [], 'price_unit': 750.0, 'product_id': product5.id, 'price_subtotal': 750.0, 'price_subtotal_incl': 750.0, 'tax_ids': [[6, False, []]], 'qty': 1, }]], 'name': 'Order 12345-123-1234', 'partner_id': self.partner1.id, 'pos_session_id': pos_session.id, 'sequence_number': 2, 'statement_ids': [[0, 0, { 'amount': 460, 'name': fields.Datetime.now(), 'payment_method_id': self.cash_payment_method.id }], [0, 0, { 'amount': 300, 'name': fields.Datetime.now(), 'payment_method_id': self.credit_payment_method.id }]], 'uid': '12345-123-1234', 'user_id': self.env.uid, 'to_invoice': True, } } pos_order_id = self.PosOrder.create_from_ui([product5_order])[0]['id'] pos_order = self.PosOrder.search([('id', '=', pos_order_id)]) #assert account_move amount_residual is 300 self.assertEqual(pos_order.account_move.amount_residual, 300) def test_sale_order_postponed_invoicing(self): """ Test the flow of creating an invoice later, after the POS session has been closed and everything has been processed. The process should: - Create a new misc entry, that will revert part of the POS closing entry. - Create the move and associating payment(s) entry, as it would do when closing with invoice. - Reconcile the receivable lines from the created misc entry with the ones from the created payment(s) """ # Create the order on the first of january. pos_order = self._create_pos_order_for_postponed_invoicing() self.assertFalse(pos_order.account_move.exists()) # Client is back on the 3rd, asks for an invoice. with freeze_time('2020-01-03'): # We set the partner on the order pos_order.partner_id = self.partner1.id pos_order.action_pos_order_invoice() # We should now have: an invoice, a payment, and a misc entry reconciled with the payment that reverse the original POS closing entry. invoice = pos_order.account_move closing_entry = pos_order.session_move_id # This search isn't the best, but we don't have any references to this move stored on other models. misc_reversal_entry = self.env['account.move'].search([('ref', '=', f'Reversal of POS closing entry {closing_entry.name} for order {pos_order.name} from session {pos_order.session_id.name}')]) # In this case we will have only one, for cash payment payment = self.env['account.move'].search([('ref', '=like', f'Invoice payment for {pos_order.name} ({pos_order.account_move.name}) using {self.cash_payment_method.name}')]) # And thus only one bank statement for it statement = self.env['account.move'].search([('journal_id', '=', self.company_data['default_journal_cash'].id)]) self.assertTrue(invoice.exists() and closing_entry.exists() and misc_reversal_entry.exists() and payment.exists()) # Check 1: Check that we have reversed every credit line on the closing entry. for closing_entry_line, misc_reversal_entry_line in zip(closing_entry.line_ids, misc_reversal_entry.line_ids): if closing_entry_line.balance < 0: self.assertEqual(closing_entry_line.balance, -misc_reversal_entry_line.balance) self.assertEqual(closing_entry_line.account_id, misc_reversal_entry_line.account_id) # Check 2: Reconciliation # The invoice receivable should be reconciled with the payment receivable of the same account. invoice_receivable_line = invoice.line_ids.filtered(lambda line: line.account_id == self.company_data['default_account_receivable']) payment_receivable_line = payment.line_ids.filtered(lambda line: line.account_id == self.company_data['default_account_receivable']) self.assertEqual(invoice_receivable_line.matching_number, payment_receivable_line.matching_number) # The payment receivable (POS) is reconciled with the closing entry receivable (POS) payment_receivable_pos_line = payment.line_ids.filtered(lambda line: line.account_id == self.company_data['company'].account_default_pos_receivable_account_id) misc_receivable_pos_line = misc_reversal_entry.line_ids.filtered(lambda line: line.account_id == self.company_data['company'].account_default_pos_receivable_account_id) self.assertEqual(misc_receivable_pos_line.matching_number, payment_receivable_pos_line.matching_number) # The closing entry receivable is reconciled with the bank statement closing_entry_receivable_line = closing_entry.line_ids.filtered(lambda line: line.account_id == self.company_data['default_account_receivable']) # Because the payment method use the default receivable statement_receivable_line = statement.line_ids.filtered(lambda line: line.account_id == self.company_data['default_account_receivable'] and line.name == pos_order.session_id.name) # Because the payment method use the default receivable self.assertEqual(closing_entry_receivable_line.matching_number, statement_receivable_line.matching_number) def test_sale_order_postponed_invoicing_anglosaxon(self): """ Test the flow of creating an invoice later, after the POS session has been closed and everything has been processed in the case of anglo-saxon accounting. """ self.env.company.anglo_saxon_accounting = True self.env.company.point_of_sale_update_stock_quantities = 'closing' pos_order = self._create_pos_order_for_postponed_invoicing() with freeze_time('2020-01-03'): # We set the partner on the order pos_order.partner_id = self.partner1.id pos_order.action_pos_order_invoice() picking_ids = pos_order.session_id.picking_ids # only one product is leaving stock self.assertEqual(sum(picking_ids.move_line_ids.mapped('quantity')), 1) def test_order_pos_tax_same_as_company(self): """Test that when the default_pos_receivable_account and the partner account_receivable are the same, payment are correctly reconciled and the invoice is correctly marked as paid. """ self.pos_config.open_ui() current_session = self.pos_config.current_session_id current_session.company_id.account_default_pos_receivable_account_id = self.partner1.property_account_receivable_id product5_order = {'data': {'amount_paid': 750, 'amount_tax': 0, 'amount_return':0, 'amount_total': 750, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, 'lines': [[0, 0, { 'discount': 0, 'pack_lot_ids': [], 'price_unit': 750.0, 'product_id': self.product3.id, 'price_subtotal': 750.0, 'price_subtotal_incl': 750.0, 'tax_ids': [[6, False, []]], 'qty': 1, }]], 'name': 'Order 12345-123-1234', 'partner_id': self.partner1.id, 'pos_session_id': current_session.id, 'sequence_number': 2, 'statement_ids': [[0, 0, { 'amount': 450, 'name': fields.Datetime.now(), 'payment_method_id': self.cash_payment_method.id }], [0, 0, { 'amount': 300, 'name': fields.Datetime.now(), 'payment_method_id': self.bank_payment_method.id }]], 'uid': '12345-123-1234', 'user_id': self.env.uid, 'to_invoice': True, } } pos_order_id = self.PosOrder.create_from_ui([product5_order])[0]['id'] pos_order = self.PosOrder.search([('id', '=', pos_order_id)]) self.assertEqual(pos_order.account_move.amount_residual, 0) def test_order_refund_with_owner(self): # open pos session self.pos_config.open_ui() current_session = self.pos_config.current_session_id # set up product iwith SN tracing and create two lots (1001, 1002) self.stock_location = self.company_data['default_warehouse'].lot_stock_id self.product2 = self.env['product.product'].create({ 'name': 'Product A', 'type': 'product', 'categ_id': self.env.ref('product.product_category_all').id, }) self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': self.product2.id, 'inventory_quantity': 1, 'location_id': self.stock_location.id, 'owner_id': self.partner1.id }).action_apply_inventory() # create pos order with the two SN created before order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product2.id, 'price_unit': 6, 'discount': 0, 'qty': 1, 'tax_ids': [[6, False, []]], 'price_subtotal': 6, 'price_subtotal_incl': 6, })], 'pricelist_id': self.pos_config.pricelist_id.id, 'amount_paid': 6.0, 'amount_total': 6.0, 'amount_tax': 0.0, 'amount_return': 0.0, 'to_invoice': False, 'last_order_preparation_change': '{}' }) payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() # I create a refund refund_action = order.refund() refund = self.PosOrder.browse(refund_action['res_id']) payment_context = {"active_ids": refund.ids, "active_id": refund.id} refund_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': refund.amount_total, 'payment_method_id': self.cash_payment_method.id, }) # I click on the validate button to register the payment. refund_payment.with_context(**payment_context).check() current_session.action_pos_session_closing_control() self.assertEqual(refund.picking_ids.move_line_ids_without_package.owner_id.id, order.picking_ids.move_line_ids_without_package.owner_id.id, "The owner of the refund is not the same as the owner of the original order") def test_journal_entries_category_without_account(self): #create a new product category without account category = self.env['product.category'].create({ 'name': 'Category without account', 'property_account_income_categ_id': False, 'property_account_expense_categ_id': False, }) product = self.env['product.product'].create({ 'name': 'Product with category without account', 'type': 'product', 'categ_id': category.id, 'property_account_income_id': False, 'property_account_expense_id': False, }) account = self.env['account.account'].create({ 'name': 'Account for category without account', 'code': 'X1111', }) self.pos_config.journal_id.default_account_id = account.id #create a new pos order with the product self.pos_config.open_ui() current_session = self.pos_config.current_session_id order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'pricelist_id': self.partner1.property_product_pricelist.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': product.id, 'price_unit': 10, 'discount': 0.0, 'qty': 1, 'tax_ids': [], 'price_subtotal': 10, 'price_subtotal_incl': 10, })], 'amount_total': 10, 'amount_tax': 0.0, 'amount_paid': 10, 'amount_return': 0.0, 'to_invoice': False, 'last_order_preparation_change': '{}' }) #create a payment payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() current_session.action_pos_session_closing_control() self.assertEqual(current_session.move_id.line_ids[0].account_id.id, account.id) def test_tracked_product_with_owner(self): # open pos session self.pos_config.open_ui() current_session = self.pos_config.current_session_id # set up product iwith SN tracing and create two lots (1001, 1002) self.stock_location = self.company_data['default_warehouse'].lot_stock_id self.product2 = self.env['product.product'].create({ 'name': 'Product A', 'type': 'product', 'tracking': 'serial', 'categ_id': self.env.ref('product.product_category_all').id, }) lot1 = self.env['stock.lot'].create({ 'name': '1001', 'product_id': self.product2.id, 'company_id': self.env.company.id, }) self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1, lot_id=lot1, owner_id=self.partner1) # create pos order with the two SN created before order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'id': 1, 'product_id': self.product2.id, 'price_unit': 6, 'discount': 0, 'qty': 1, 'tax_ids': [[6, False, []]], 'price_subtotal': 6, 'price_subtotal_incl': 6, 'pack_lot_ids': [ [0, 0, {'lot_name': '1001'}], ] })], 'pricelist_id': self.pos_config.pricelist_id.id, 'amount_paid': 6.0, 'amount_total': 6.0, 'amount_tax': 0.0, 'amount_return': 0.0, 'to_invoice': False, 'last_order_preparation_change': '{}' }) payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() current_session.action_pos_session_closing_control() self.assertEqual(current_session.picking_ids.move_line_ids.owner_id.id, self.partner1.id) def test_order_refund_with_invoice(self): """This test make sure that credit notes of pos orders are correctly linked to the original invoice.""" self.pos_config.open_ui() current_session = self.pos_config.current_session_id order_data = {'data': {'amount_paid': 450, 'amount_tax': 0, 'amount_return': 0, 'amount_total': 450, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, 'lines': [[0, 0, { 'discount': 0, 'pack_lot_ids': [], 'price_unit': 450.0, 'product_id': self.product3.id, 'price_subtotal': 450.0, 'price_subtotal_incl': 450.0, 'tax_ids': [[6, False, []]], 'qty': 1, }]], 'name': 'Order 12345-123-1234', 'partner_id': self.partner1.id, 'pos_session_id': current_session.id, 'sequence_number': 2, 'statement_ids': [[0, 0, { 'amount': 450, 'name': fields.Datetime.now(), 'payment_method_id': self.cash_payment_method.id }]], 'uid': '12345-123-1234', 'user_id': self.env.uid, 'to_invoice': True, } } order = self.PosOrder.create_from_ui([order_data]) order = self.PosOrder.browse(order[0]['id']) refund_id = order.refund()['res_id'] refund = self.PosOrder.browse(refund_id) context_payment = {"active_ids": refund.ids, "active_id": refund.id} refund_payment = self.PosMakePayment.with_context(**context_payment).create({ 'amount': refund.amount_total, 'payment_method_id': self.cash_payment_method.id }) refund_payment.with_context(**context_payment).check() refund.action_pos_order_invoice() #get last invoice created current_session.action_pos_session_closing_control() invoices = self.env['account.move'].search([('move_type', '=', 'out_invoice')], order='id desc', limit=1) credit_notes = self.env['account.move'].search([('move_type', '=', 'out_refund')], order='id desc', limit=1) self.assertEqual(credit_notes.ref, "Reversal of: "+invoices.name) self.assertEqual(credit_notes.reversed_entry_id.id, invoices.id) def test_invoicing_after_closing_session(self): """ Test that an invoice can be created after the session is closed """ #create customer account payment method self.customer_account_payment_method = self.env['pos.payment.method'].create({ 'name': 'Customer Account', 'split_transactions': True, }) self.product1 = self.env['product.product'].create({ 'name': 'Product A', 'type': 'product', 'categ_id': self.env.ref('product.product_category_all').id, }) self.partner1.write({'parent_id': self.env['res.partner'].create({'name': 'Parent'}).id}) #add customer account payment method to pos config self.pos_config.write({ 'payment_method_ids': [(4, self.customer_account_payment_method.id, 0)], }) # change the currency of PoS config (self.currency_data['currency'].rate_ids | self.company.currency_id.rate_ids).unlink() self.env['res.currency.rate'].create({ 'rate': 0.5, 'currency_id': self.currency_data['currency'].id, 'name': datetime.today().date(), }) self.pos_config.journal_id.write({ 'currency_id': self.currency_data['currency'].id }) other_pricelist = self.env['product.pricelist'].create({ 'name': 'Public Pricelist Other', 'currency_id': self.currency_data['currency'].id, }) self.pos_config.write({ 'pricelist_id': other_pricelist.id, 'available_pricelist_ids': [(6, 0, other_pricelist.ids)], }) self.pos_config.open_ui() current_session = self.pos_config.current_session_id # create pos order order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product1.id, 'price_unit': 6, 'discount': 0, 'qty': 1, 'tax_ids': [[6, False, []]], 'price_subtotal': 6, 'price_subtotal_incl': 6, })], 'pricelist_id': self.pos_config.pricelist_id.id, 'amount_paid': 6.0, 'amount_total': 6.0, 'amount_tax': 0.0, 'amount_return': 0.0, 'to_invoice': False, 'last_order_preparation_change': '{}' }) #pay for the order with customer account payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': 2.0, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': 4.0, 'payment_method_id': self.customer_account_payment_method.id }) order_payment.with_context(**payment_context).check() # close session current_session.action_pos_session_closing_control() # create invoice order.action_pos_order_invoice() #get journal entry that does the reverse payment, it the ref must contains Reversal reverse_payment = self.env['account.move'].search([('ref', 'ilike', "Reversal")]) original_payment = self.env['account.move'].search([('ref', '=', current_session.display_name)]) original_customer_payment_entry = original_payment.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable') reverser_customer_payment_entry = reverse_payment.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable') #check that both use the same account self.assertEqual(len(reverser_customer_payment_entry), 2) self.assertTrue(order.account_move.line_ids.partner_id == self.partner1.commercial_partner_id) self.assertEqual(reverser_customer_payment_entry[0].balance, -4.0) self.assertEqual(reverser_customer_payment_entry[1].balance, -8.0) self.assertEqual(reverser_customer_payment_entry[0].amount_currency, -2.0) self.assertEqual(reverser_customer_payment_entry[1].amount_currency, -4.0) self.assertEqual(original_customer_payment_entry.account_id.id, reverser_customer_payment_entry.account_id.id) self.assertEqual(reverser_customer_payment_entry.partner_id, original_customer_payment_entry.partner_id) def test_order_total_subtotal_account_line_values(self): self.tax1 = self.env['account.tax'].create({ 'name': 'Tax 1', 'amount': 10, 'amount_type': 'percent', 'type_tax_use': 'sale', }) #create an account to be used as income account self.account1 = self.env['account.account'].create({ 'name': 'Account 1', 'code': 'AC1', 'account_type': 'income', 'reconcile': True, }) self.product1 = self.env['product.product'].create({ 'name': 'Product A', 'type': 'product', 'taxes_id': [(6, 0, self.tax1.ids)], 'categ_id': self.env.ref('product.product_category_all').id, 'property_account_income_id': self.account1.id, }) self.product2 = self.env['product.product'].create({ 'name': 'Product B', 'type': 'product', 'taxes_id': [(6, 0, self.tax1.ids)], 'categ_id': self.env.ref('product.product_category_all').id, 'property_account_income_id': self.account1.id, }) self.pos_config.open_ui() #create an order with product1 order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': self.pos_config.current_session_id.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product1.id, 'price_unit': 100, 'discount': 0, 'qty': 1, 'tax_ids': [[6, False, [self.tax1.id]]], 'price_subtotal': 100, 'price_subtotal_incl': 110, }), (0, 0, { 'name': "OL/0002", 'product_id': self.product2.id, 'price_unit': 100, 'discount': 0, 'qty': 1, 'tax_ids': [[6, False, [self.tax1.id]]], 'price_subtotal': 100, 'price_subtotal_incl': 110, })], 'pricelist_id': self.pos_config.pricelist_id.id, 'amount_paid': 220.0, 'amount_total': 220.0, 'amount_tax': 20.0, 'amount_return': 0.0, 'to_invoice': False, 'last_order_preparation_change': '{}' }) #make payment payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() session_id = self.pos_config.current_session_id # closing the session with basic pos access pos_user = self.env['res.users'].create({ 'name': "PoS user", 'login': "pos_user", 'email': "pos_user@yourcompany.com", 'groups_id': [(6, 0, [self.ref('base.group_user'), self.ref('point_of_sale.group_pos_user')])], }) self.pos_config.current_session_id.with_user(pos_user).action_pos_session_closing_control() #get journal entries created aml = session_id.move_id.line_ids.filtered(lambda x: x.account_id == self.account1 and x.tax_ids == self.tax1) self.assertEqual(aml.price_total, 220) self.assertEqual(aml.price_subtotal, 200) def test_multi_exp_account_real_time(self): #Create a real time valuation product category self.real_time_categ = self.env['product.category'].create({ 'name': 'test category', 'parent_id': False, 'property_cost_method': 'fifo', 'property_valuation': 'real_time', }) #Create 2 accounts to be used for each product self.account1 = self.env['account.account'].create({ 'name': 'Account 1', 'code': 'AC1', 'reconcile': True, 'account_type': 'expense', }) self.account2 = self.env['account.account'].create({ 'name': 'Account 1', 'code': 'AC2', 'reconcile': True, 'account_type': 'expense', }) self.product_a = self.env['product.product'].create({ 'name': 'Product A', 'type': 'product', 'categ_id': self.real_time_categ.id, 'property_account_expense_id': self.account1.id, 'property_account_income_id': self.account1.id, }) self.product_b = self.env['product.product'].create({ 'name': 'Product B', 'type': 'product', 'categ_id': self.real_time_categ.id, 'property_account_expense_id': self.account2.id, 'property_account_income_id': self.account2.id, }) #Create an order with the 2 products self.pos_config.open_ui() order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': self.pos_config.current_session_id.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product_a.id, 'price_unit': 100, 'discount': 0, 'qty': 1, 'tax_ids': [], 'price_subtotal': 100, 'price_subtotal_incl': 100, }), (0, 0, { 'name': "OL/0002", 'product_id': self.product_b.id, 'price_unit': 100, 'discount': 0, 'qty': 1, 'tax_ids': [], 'price_subtotal': 100, 'price_subtotal_incl': 100, })], 'pricelist_id': self.pos_config.pricelist_id.id, 'amount_paid': 200.0, 'amount_total': 200.0, 'amount_tax': 0.0, 'amount_return': 0.0, 'to_invoice': False, 'last_order_preparation_change': '{}', 'shipping_date': fields.Date.today(), }) #make payment payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() self.pos_config.current_session_id.action_pos_session_closing_control() order.picking_ids._action_done() moves = self.env['account.move'].search([('ref', '=', f'pos_order_{order.id}')]) self.assertEqual(len(moves), 2) def test_no_default_pricelist(self): """Should not have default_pricelist if use_pricelist is false.""" pricelist = self.env['product.pricelist'].create({ 'name': 'Test Pricelist', }) self.pos_config.write({ 'pricelist_id': pricelist.id, 'use_pricelist': False, }) self.pos_config.open_ui() loaded_data = self.pos_config.current_session_id.load_pos_data() self.assertFalse(loaded_data.get('default_pricelist', False)) def test_refund_rounding_backend(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': 49.99, 'taxes_id': False, }) self.pos_config.write({ 'rounding_method': rouding_method.id, 'cash_rounding': True, 'only_round_cash_method': True, }) self.pos_config.open_ui() current_session = self.pos_config.current_session_id order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': False, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.env['product.product'].search([('available_in_pos', '=', True)], limit=1).id, 'price_unit': 49.99, 'discount': 0, 'qty': 1, 'tax_ids': [], 'price_subtotal': 49.99, 'price_subtotal_incl': 49.99, })], 'pricelist_id': False, 'amount_paid': 50.0, 'amount_total': 49.99, 'amount_tax': 0.0, 'amount_return': 0.0, 'to_invoice': False, 'last_order_preparation_change': {} }) #make payment payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.cash_payment_method.id }) order_payment.with_context(**payment_context).check() refund = order.refund() refund = self.PosOrder.browse(refund['res_id']) payment_context = {"active_ids": refund.ids, "active_id": refund.id} refund_payment = self.PosMakePayment.with_context(**payment_context).create({ 'payment_method_id': self.cash_payment_method.id }) self.assertEqual(refund_payment.amount, -50.0) refund_payment.with_context(**payment_context).check() current_session.action_pos_session_closing_control() self.assertEqual(refund.amount_total, -49.99) self.assertEqual(refund.amount_paid, -50.0) self.assertEqual(current_session.state, 'closed') def test_order_different_lots(self): self.pos_config.open_ui() current_session = self.pos_config.current_session_id self.stock_location = self.company_data['default_warehouse'].lot_stock_id self.product2 = self.env['product.product'].create({ 'name': 'Product A', 'type': 'product', 'tracking': 'lot', }) lot1 = self.env['stock.lot'].create({ 'name': '1001', 'product_id': self.product2.id, }) lot2 = self.env['stock.lot'].create({ 'name': '1002', 'product_id': self.product2.id, }) quant1 = self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': self.product2.id, 'inventory_quantity': 5, 'location_id': self.stock_location.id, 'lot_id': lot1.id }) quant1.action_apply_inventory() quant2 = self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': self.product2.id, 'inventory_quantity': 5, 'location_id': self.stock_location.id, 'lot_id': lot2.id }) quant2.action_apply_inventory() order = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, 'partner_id': self.partner1.id, 'lines': [(0, 0, { 'name': "OL/0001", 'product_id': self.product2.id, 'price_unit': 6, 'discount': 0, 'qty': 2, 'tax_ids': [[6, False, []]], 'price_subtotal': 12, 'price_subtotal_incl': 12, 'pack_lot_ids': [ [0, 0, {'lot_name': '1001'}], ] }), (0, 0, { 'name': "OL/0002", 'product_id': self.product2.id, 'price_unit': 6, 'discount': 0, 'qty': 1, 'tax_ids': [[6, False, []]], 'price_subtotal': 6, 'price_subtotal_incl': 6, 'pack_lot_ids': [ [0, 0, {'lot_name': '1002'}], ] })], 'pricelist_id': self.pos_config.pricelist_id.id, 'amount_paid': 18.0, 'amount_total': 18.0, 'amount_tax': 0.0, 'amount_return': 0.0, 'to_invoice': False, 'last_order_preparation_change': '{}' }) payment_context = {"active_ids": order.ids, "active_id": order.id} order_payment = self.PosMakePayment.with_context(**payment_context).create({ 'amount': order.amount_total, 'payment_method_id': self.bank_payment_method.id }) order_payment.with_context(**payment_context).check() self.pos_config.current_session_id.action_pos_session_closing_control() self.assertEqual(quant2.quantity, 4) self.assertEqual(quant1.quantity, 3)