diff --git a/addons/l10n_ar_pos/tests/test_pos_ar.py b/addons/l10n_ar_pos/tests/test_pos_ar.py index 19e9e2ce409a7d..de501375daff22 100644 --- a/addons/l10n_ar_pos/tests/test_pos_ar.py +++ b/addons/l10n_ar_pos/tests/test_pos_ar.py @@ -29,6 +29,7 @@ def setUpClass(cls): cls.bank_pm = cls.env['pos.payment.method'].sudo().create({ 'name': 'Bank', + 'type': 'bank', 'journal_id': cls.company_data['default_journal_bank'].id, 'receivable_account_id': cls.pos_receivable_bank.id, 'outstanding_account_id': cls.outstanding_bank.id, @@ -36,6 +37,7 @@ def setUpClass(cls): }) cls.cash_pm = cls.env['pos.payment.method'].sudo().create({ 'name': 'Cash', + 'type': 'cash', 'journal_id': cls.company_data['default_journal_cash'].id, 'receivable_account_id': cls.pos_receivable_bank.id, 'outstanding_account_id': cls.outstanding_bank.id, diff --git a/addons/l10n_ar_pos/views/templates.xml b/addons/l10n_ar_pos/views/templates.xml index 3920a4d86b5ebc..ea0ef6facc26ab 100644 --- a/addons/l10n_ar_pos/views/templates.xml +++ b/addons/l10n_ar_pos/views/templates.xml @@ -8,7 +8,7 @@ pos_order.company_id.country_code != 'AR' -

Invoice not available. You can contact us for more info

+

Invoice not available. You can contact us for more info

diff --git a/addons/l10n_be_pos_sale/tests/test_l10n_be_pos_sale.py b/addons/l10n_be_pos_sale/tests/test_l10n_be_pos_sale.py index 593a63988017b0..4b468656342a76 100644 --- a/addons/l10n_be_pos_sale/tests/test_l10n_be_pos_sale.py +++ b/addons/l10n_be_pos_sale/tests/test_l10n_be_pos_sale.py @@ -92,7 +92,6 @@ def test_pos_branch_company_access(self): b_pos_config = self.env['pos.config'].with_company(branch).create({ 'name': 'Main', 'journal_id': self.company_data['default_journal_sale'].id, - 'invoice_journal_id': self.company_data['default_journal_sale'].id, 'payment_method_ids': [(4, bank_payment_method.id)], }) diff --git a/addons/l10n_ch_pos/tests/test_frontend.py b/addons/l10n_ch_pos/tests/test_frontend.py index fc079a3307dddb..94f247ba3df54c 100644 --- a/addons/l10n_ch_pos/tests/test_frontend.py +++ b/addons/l10n_ch_pos/tests/test_frontend.py @@ -17,7 +17,7 @@ def _get_main_company(cls): def test_l10n_ch_pos_pay_later_invoice_has_bank_partner(self): customer_account_payment_method = self.env['pos.payment.method'].create({ 'name': 'Customer Account', - 'split_transactions': True, + 'type': 'pay_later', }) self.main_pos_config.write({ 'payment_method_ids': [Command.link(customer_account_payment_method.id)], diff --git a/addons/l10n_es_edi_tbai_pos/models/pos_order.py b/addons/l10n_es_edi_tbai_pos/models/pos_order.py index 4d42af49639bb4..bf96f495463d1a 100644 --- a/addons/l10n_es_edi_tbai_pos/models/pos_order.py +++ b/addons/l10n_es_edi_tbai_pos/models/pos_order.py @@ -166,7 +166,7 @@ def _l10n_es_tbai_create_edi_document(self, cancel=False): def _l10n_es_tbai_get_values(self): self.ensure_one() - base_lines = self.lines._prepare_tax_base_line_values() + base_lines = self.lines._prepare_base_lines_for_taxes_computation() for base_line in base_lines: base_line['name'] = base_line['record'].name self.env['l10n_es_edi_tbai.document']._add_base_lines_tax_amounts(base_lines, self.company_id) diff --git a/addons/l10n_es_edi_verifactu_pos/models/pos_order.py b/addons/l10n_es_edi_verifactu_pos/models/pos_order.py index 7f4c98be53c797..d9794b81e5efc2 100644 --- a/addons/l10n_es_edi_verifactu_pos/models/pos_order.py +++ b/addons/l10n_es_edi_verifactu_pos/models/pos_order.py @@ -191,7 +191,7 @@ def _l10n_es_edi_verifactu_get_record_values(self, cancellation=False): 'clave_regimen': clave_regimen, }) - base_lines = self.lines._prepare_tax_base_line_values() + base_lines = self.lines._prepare_base_lines_for_taxes_computation() vals['tax_details'] = self.env['l10n_es_edi_verifactu.document']._get_tax_details(base_lines, company) return vals diff --git a/addons/l10n_es_edi_verifactu_pos/tests/test_pos_order.py b/addons/l10n_es_edi_verifactu_pos/tests/test_pos_order.py index 39f7eb6fa8de8d..41fc27ed1f5849 100644 --- a/addons/l10n_es_edi_verifactu_pos/tests/test_pos_order.py +++ b/addons/l10n_es_edi_verifactu_pos/tests/test_pos_order.py @@ -32,8 +32,10 @@ def setUpClass(cls): def with_pos_session(self): session = self.open_new_session(0.0) yield session - session.post_closing_cash_details(0.0) - session.close_session_from_ui() + cash_pm = self.config._get_cash_payment_method() + session.close_session_from_ui({ + cash_pm.id: 0, + }) def _create_order(self, data): date_order = data.pop('date_order', None) @@ -140,7 +142,7 @@ def test_order_not_invoiced(self): }) refund.l10n_es_edi_verifactu_refund_reason = 'R5' refund_payment.with_context(**payment_context).check() - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() self.assertRecordValues(order, [{ 'l10n_es_edi_verifactu_state': 'accepted', diff --git a/addons/l10n_es_pos/static/tests/tours/spanish_pos_tour.js b/addons/l10n_es_pos/static/tests/tours/spanish_pos_tour.js index 12db6a302401b4..856904bbe9b27e 100644 --- a/addons/l10n_es_pos/static/tests/tours/spanish_pos_tour.js +++ b/addons/l10n_es_pos/static/tests/tours/spanish_pos_tour.js @@ -93,17 +93,9 @@ registry.category("web_tour.tours").add("l10n_es_pos_settle_account_due", { Dialog.confirm("Open Register"), ProductScreen.clickPartnerButton(), PartnerList.settleCustomerAccount("Partner Test 1", "10.0", "TSJ/", "/00001", true), - ProductScreen.clickPayButton(), PaymentScreen.clickPaymentMethod("Bank"), PaymentScreen.clickValidate(), - Chrome.confirmPopup(), - FeedbackScreen.isShown(), - FeedbackScreen.checkTicketData({ - payment_lines: [ - { name: "Bank", amount: "10.0" }, - { name: "Customer Account", amount: "-10.0" }, - ], - }), + ProductScreen.isShown(), Chrome.endTour(), ].flat(), }); diff --git a/addons/l10n_es_pos/tests/test_frontend.py b/addons/l10n_es_pos/tests/test_frontend.py index b71d1f7af0ceb6..883111923faa6d 100644 --- a/addons/l10n_es_pos/tests/test_frontend.py +++ b/addons/l10n_es_pos/tests/test_frontend.py @@ -19,7 +19,7 @@ def _get_main_company(cls): def test_spanish_pos(self): split_payment_method = self.env['pos.payment.method'].create({ 'name': 'Customer Account', - 'split_transactions': True, + 'type': 'pay_later', }) self.main_pos_config.payment_method_ids = [(4, split_payment_method.id)] @@ -30,7 +30,7 @@ def test_spanish_pos(self): 'code': 'SIMP', }) def get_number_of_regular_invoices(): - return self.env['account.move'].search_count([('journal_id', '=', self.main_pos_config.invoice_journal_id.id), ('l10n_es_is_simplified', '=', False), ('pos_order_ids', '!=', False)]) + return self.env['account.move'].search_count([('journal_id', '=', self.main_pos_config.journal_id.id), ('l10n_es_is_simplified', '=', False), ('pos_order_ids', '!=', False)]) initial_number_of_regular_invoices = get_number_of_regular_invoices() self.main_pos_config.l10n_es_simplified_invoice_journal_id = simp # this `limit` value is linked to the `SIMPLIFIED_INVOICE_LIMIT` const in the tour @@ -49,7 +49,7 @@ def test_l10n_es_pos_reconcile(self): # create customer account payment method self.customer_account_payment_method = self.env['pos.payment.method'].create({ 'name': 'Customer Account', - 'split_transactions': True, + 'type': 'pay_later', }) # add customer account payment method to pos config self.main_pos_config.write({ @@ -89,7 +89,7 @@ def test_l10n_es_pos_reconcile(self): order_payment.with_context(**payment_context).check() self.assertEqual(self.partner_test_1.total_due, 10) - current_session.action_pos_session_closing_control() + current_session.close_session_from_ui() self.main_pos_config.with_user(self.pos_admin).open_ui() self.start_tour("/pos/ui/%d" % self.main_pos_config.id, 'l10n_es_pos_settle_account_due', login="accountman") @@ -118,11 +118,11 @@ def test_spanish_pos_invoice_no_certificate(self): 'price_unit': 100, 'qty': 1.0, 'tax_ids': self.product_a.taxes_id, - 'price_subtotal': 85, - 'price_subtotal_incl': 100, + 'price_subtotal': 100, + 'price_subtotal_incl': 115, 'discount': 0, })], - 'amount_total': 100, + 'amount_total': 115, 'amount_tax': 15, 'amount_paid': 0, 'amount_return': 0, @@ -131,7 +131,7 @@ def test_spanish_pos_invoice_no_certificate(self): context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} self.pos_make_payment_0 = self.env['pos.make.payment'].with_context(context_make_payment).create({ - 'amount': 100.0, + 'amount': 115.0, 'payment_method_id': self.main_pos_config.payment_method_ids[0].id, }) context_payment = {'active_id': self.pos_order_pos0.id} diff --git a/addons/l10n_fr_pos_cert/tests/test_hash.py b/addons/l10n_fr_pos_cert/tests/test_hash.py index 215976de6a9d17..213ba91ef3d939 100644 --- a/addons/l10n_fr_pos_cert/tests/test_hash.py +++ b/addons/l10n_fr_pos_cert/tests/test_hash.py @@ -13,6 +13,10 @@ def setUpClass(cls): super().setUpClass() def test_hashes_should_be_equal_if_no_alteration(self): + journal = self.env['account.journal']._ensure_company_account_journal() + if self.pos_config_usd.journal_id != journal: + self.pos_config_usd.journal_id = journal + product1 = self.env['product.product'].create({ 'name': 'product1', }) @@ -79,6 +83,6 @@ def test_hashes_should_be_equal_if_no_alteration(self): posted_order = self.env['pos.order'].search([('uuid', '=', '12345-123-1234')]) self.assertEqual(posted_order.state, 'paid') - self.pos_config_usd.current_session_id.action_pos_session_closing_control() + self.pos_config_usd.current_session_id.close_session_from_ui() self.assertEqual(posted_order.l10n_fr_hash, posted_order._compute_hash('')) diff --git a/addons/l10n_fr_pos_cert/tests/test_string_to_hash.py b/addons/l10n_fr_pos_cert/tests/test_string_to_hash.py index ad7a1f883c6450..9425b461dcfc82 100644 --- a/addons/l10n_fr_pos_cert/tests/test_string_to_hash.py +++ b/addons/l10n_fr_pos_cert/tests/test_string_to_hash.py @@ -110,5 +110,5 @@ def test_string_to_hash(self): {'amount': 8900, 'payment_method': self.cash_pm1}, {'amount': 11000, 'payment_method': self.pay_later_pm} ]) - self.basic_config.current_session_id.action_pos_session_closing_control() + self.basic_config.current_session_id.close_session_from_ui() self.assertEqual(order.l10n_fr_string_to_hash, self._compute_string_to_hash_original(order)) diff --git a/addons/l10n_id_pos/tests/test_qris_pos.py b/addons/l10n_id_pos/tests/test_qris_pos.py index 00f2d40d73f026..cc135215856ccc 100644 --- a/addons/l10n_id_pos/tests/test_qris_pos.py +++ b/addons/l10n_id_pos/tests/test_qris_pos.py @@ -56,13 +56,14 @@ def setUpClass(cls): cls.bank_pm = cls.env['pos.payment.method'].sudo().create({ 'name': 'Cash', - 'journal_id': cls.company_data['default_journal_bank'].id, + 'type': 'cash', + 'journal_id': cls.company_data['default_journal_cash'].id, 'receivable_account_id': cls.pos_receivable_bank.id, - 'outstanding_account_id': cls.outstanding_bank.id, 'company_id': cls.company.id, }) cls.qris_pm = cls.env['pos.payment.method'].sudo().create({ 'name': 'QRIS', + 'type': 'bank', 'journal_id': cls.company_data['default_journal_bank'].id, 'receivable_account_id': cls.pos_receivable_bank.id, 'outstanding_account_id': cls.outstanding_bank.id, diff --git a/addons/l10n_in_pos/models/account_move.py b/addons/l10n_in_pos/models/account_move.py index 3d0c9532cf826b..3d3d015c3bc333 100644 --- a/addons/l10n_in_pos/models/account_move.py +++ b/addons/l10n_in_pos/models/account_move.py @@ -8,7 +8,7 @@ class AccountMove(models.Model): @api.depends('pos_session_ids', 'reversed_pos_order_id') def _compute_l10n_in_state_id(self): res = super()._compute_l10n_in_state_id() - to_compute = self.filtered(lambda m: m.country_code == 'IN' and not m.l10n_in_state_id and m.journal_id.type == 'general' and (m.pos_session_ids or m.reversed_pos_order_id)) + to_compute = self.filtered(lambda m: m.country_code == 'IN' and not m.l10n_in_state_id and m.journal_id.type == 'sale' and (m.pos_session_ids or m.reversed_pos_order_id)) for move in to_compute: move.l10n_in_state_id = move.company_id.state_id return res diff --git a/addons/l10n_in_pos/models/pos_order.py b/addons/l10n_in_pos/models/pos_order.py index 065a231bc50cc9..854a3117673d97 100644 --- a/addons/l10n_in_pos/models/pos_order.py +++ b/addons/l10n_in_pos/models/pos_order.py @@ -4,10 +4,35 @@ class PosOrder(models.Model): _inherit = 'pos.order' - def _prepare_product_aml_dict(self, base_line_vals, update_base_line_vals, rate, sign): - res = super()._prepare_product_aml_dict(base_line_vals, update_base_line_vals, rate, sign) - if self.company_id.account_fiscal_country_id.code == 'IN': - res.update({ - 'l10n_in_hsn_code': base_line_vals['l10n_in_hsn_code'], - }) + def _grouping_function(self, line): + res = super()._grouping_function(line) # Returns mappingproxy (immutable) + if self.config_id.company_id.l10n_in_is_gst_registered: + res = dict(res) # Convert to mutable dict + res['l10n_in_hsn_code'] = line.get('l10n_in_hsn_code', '') + res['product_uom_id'] = line.get('product_uom_id', '') return res + + def _prepare_account_move_line_data(self, aggregate=True): + lines = super()._prepare_account_move_line_data(aggregate) + if aggregate: + return lines + + for line in lines: + move = line.get('account.move.line') + meta = line.get('metadata') + + if self.config_id.company_id.l10n_in_is_gst_registered and meta: + pos_line = meta.get('line') if meta else None + if not pos_line: + continue + move["l10n_in_hsn_code"] = pos_line.l10n_in_hsn_code + move["product_uom_id"] = pos_line.product_uom_id.id + + return lines + + def _prepare_move_line_vals_from_base_line(self, base_line): + values = super()._prepare_move_line_vals_from_base_line(base_line) + if self.config_id.company_id.l10n_in_is_gst_registered: + values["l10n_in_hsn_code"] = base_line.get("l10n_in_hsn_code", "") + values["product_uom_id"] = base_line.get("product_uom_id", "").id + return values diff --git a/addons/l10n_in_pos/models/pos_order_line.py b/addons/l10n_in_pos/models/pos_order_line.py index 3e96068abcf85d..03090a0077bf45 100644 --- a/addons/l10n_in_pos/models/pos_order_line.py +++ b/addons/l10n_in_pos/models/pos_order_line.py @@ -24,10 +24,11 @@ def _load_pos_data_fields(self, config): params += ['l10n_in_hsn_code'] return params - def _prepare_base_line_for_taxes_computation(self): - res = super()._prepare_base_line_for_taxes_computation() + def _prepare_base_lines_for_taxes_computation(self): + base_lines = super()._prepare_base_lines_for_taxes_computation() if self.company_id.l10n_in_is_gst_registered: - res.update({ - 'l10n_in_hsn_code': self.l10n_in_hsn_code, - }) - return res + for index, line in enumerate(self): + base_lines[index].update({ + 'l10n_in_hsn_code': line.l10n_in_hsn_code, + }) + return base_lines diff --git a/addons/l10n_in_pos/models/pos_session.py b/addons/l10n_in_pos/models/pos_session.py index 08897fa3699f59..9f513fbb890c7b 100644 --- a/addons/l10n_in_pos/models/pos_session.py +++ b/addons/l10n_in_pos/models/pos_session.py @@ -4,24 +4,6 @@ class PosSession(models.Model): _inherit = 'pos.session' - def _get_sale_key(self, base_line): - res = super()._get_sale_key(base_line) - if self.config_id.company_id.l10n_in_is_gst_registered: - res.update({ - 'uom_id': base_line['uom_id'].id, - 'l10n_in_hsn_code': base_line['l10n_in_hsn_code'], - }) - return res - - def _get_sale_vals(self, key, sale_vals): - res = super()._get_sale_vals(key, sale_vals) - if self.config_id.company_id.l10n_in_is_gst_registered: - res.update({ - 'l10n_in_hsn_code': key['l10n_in_hsn_code'], - 'product_uom_id': key['uom_id'], - }) - return res - def set_missing_hsn_codes_in_pos_orders(self): self.ensure_one() PosOrderLine = self.env['pos.order.line'] @@ -40,7 +22,33 @@ def set_missing_hsn_codes_in_pos_orders(self): line.l10n_in_hsn_code = line.product_id.l10n_in_hsn_code # Lines where product itself is missing HSN - missing_hsn_lines = PosOrderLine.search( + return PosOrderLine.search( base_domain + [('product_id.l10n_in_hsn_code', '=', False)] ) - return missing_hsn_lines + + def _prepare_account_move_line_commands_for_reversal(self, order, invoice_to_reverse): + commands = super()._prepare_account_move_line_commands_for_reversal(order, invoice_to_reverse) + if not order.config_id.company_id.l10n_in_is_gst_registered: + return commands + + product_lines = invoice_to_reverse.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + + for idx, line in enumerate(product_lines): + command = commands[idx] + command[2]["l10n_in_hsn_code"] = line.l10n_in_hsn_code + command[2]["product_uom_id"] = line.product_uom_id.id + + return commands + + def _validate_session_accounting(self, check_validity=True): + result = super()._validate_session_accounting(check_validity=check_validity) + gst_sessions = self.filtered( + lambda session: session.company_id.l10n_in_is_gst_registered + and (session.sales_move_id or session.refunds_move_id), + ) + if gst_sessions: + tax_tags_dict = self.env['account.move.line']._get_l10n_in_tax_tag_ids() + (gst_sessions.sales_move_id | gst_sessions.refunds_move_id).line_ids._set_l10n_in_gstr_section(tax_tags_dict) + return result diff --git a/addons/l10n_in_pos/receipt/pos_order_receipt.py b/addons/l10n_in_pos/receipt/pos_order_receipt.py index 747f62839f2bc5..725a7b40bd35e0 100644 --- a/addons/l10n_in_pos/receipt/pos_order_receipt.py +++ b/addons/l10n_in_pos/receipt/pos_order_receipt.py @@ -10,7 +10,7 @@ def order_receipt_generate_data(self, basic_receipt=False): data = super().order_receipt_generate_data(basic_receipt) if self.company_id.country_id.code == 'IN': - base_lines = self.lines._prepare_tax_base_line_values() + base_lines = self.lines._prepare_base_lines_for_taxes_computation() self.env['account.tax']._add_tax_details_in_base_lines(base_lines, self.company_id) self.env['account.tax']._round_base_lines_tax_details(base_lines, self.company_id) l10n_in_hsn_summary = self.env['account.tax']._l10n_in_get_hsn_summary_table(base_lines, False) diff --git a/addons/l10n_in_pos/tests/common.py b/addons/l10n_in_pos/tests/common.py index a0ac2f91896259..91a3a5903eaf6b 100644 --- a/addons/l10n_in_pos/tests/common.py +++ b/addons/l10n_in_pos/tests/common.py @@ -74,8 +74,10 @@ def with_pos_session(self): """Opens a new POS session and ensures it is closed properly.""" session = self.open_new_session(0.0) yield session - session.post_closing_cash_details(0.0) - session.close_session_from_ui() + cash_pm = self.config._get_cash_payment_method() + session.close_session_from_ui({ + cash_pm.id: 0, + }) def _create_order(self, ui_data): """Helper to create a POS order from UI data.""" diff --git a/addons/l10n_in_pos/tests/test_gstr_section.py b/addons/l10n_in_pos/tests/test_gstr_section.py index 954265f3f0ea5b..fc5bc35a925fe7 100644 --- a/addons/l10n_in_pos/tests/test_gstr_section.py +++ b/addons/l10n_in_pos/tests/test_gstr_section.py @@ -17,8 +17,8 @@ def test_b2cs_gstr_section_with_pos_order(self): ], 'payments': [(self.bank_pm1, 630.0)], }) - session.action_pos_session_closing_control() - pos_entry_lines = session.move_id.line_ids + session.close_session_from_ui() + pos_entry_lines = session.move_ids.line_ids for line in pos_entry_lines.filtered(lambda l: l.display_type in ('product, tax')): self.assertEqual(line.l10n_in_gstr_section, 'sale_b2cs') @@ -31,7 +31,7 @@ def test_nil_rated_gstr_section_with_pos_order(self): 'payments': [(self.bank_pm1, 900.0)], 'customer': self.partner_a, }) - session.action_pos_session_closing_control() - pos_entry_lines = session.move_id.line_ids + session.close_session_from_ui() + pos_entry_lines = session.move_ids.line_ids for line in pos_entry_lines.filtered(lambda l: l.display_type in ('product, tax')): self.assertEqual(line.l10n_in_gstr_section, 'sale_nil_rated') diff --git a/addons/l10n_in_pos/tests/test_hsn_summary.py b/addons/l10n_in_pos/tests/test_hsn_summary.py index f8576efd384ed3..f3f85bf6e7d7b5 100644 --- a/addons/l10n_in_pos/tests/test_hsn_summary.py +++ b/addons/l10n_in_pos/tests/test_hsn_summary.py @@ -22,6 +22,10 @@ def test_l10n_in_hsn_summary_pos(self): # are exactly the same. tests = self._test_l10n_in_hsn_summary_1() test1 = next(tests) + self.env.company.write({ + 'street': 'street', + 'state_id': self.env['res.country.state'].search([], limit=1).id, + }) self.ensure_products_on_document(test1[1], 'product_1') with self.with_new_session(user=self.pos_user): self.start_pos_tour('test_l10n_in_hsn_summary_pos') diff --git a/addons/l10n_in_pos/tests/test_pos_flow.py b/addons/l10n_in_pos/tests/test_pos_flow.py index d9aa04efcb3b72..d499c019f3d24c 100644 --- a/addons/l10n_in_pos/tests/test_pos_flow.py +++ b/addons/l10n_in_pos/tests/test_pos_flow.py @@ -50,7 +50,6 @@ def test_invoice_order_after_session_closed(self): # with self.with_pos_session() as session2: pos_order_going_to_invoice.partner_id = self.partner_a pos_order_going_to_invoice.action_pos_order_invoice() - # session2.action_pos_session_closing_control() # Confirm that reversal move(s) are now set reversal_moves = self.env['account.move'].search([('reversed_pos_order_id', '=', pos_order_going_to_invoice.id)]) diff --git a/addons/l10n_jo_edi_pos/models/pos_edi_ubl_21_jo.py b/addons/l10n_jo_edi_pos/models/pos_edi_ubl_21_jo.py index f0e132bd607ec5..9a95d2b884d5b1 100644 --- a/addons/l10n_jo_edi_pos/models/pos_edi_ubl_21_jo.py +++ b/addons/l10n_jo_edi_pos/models/pos_edi_ubl_21_jo.py @@ -102,7 +102,7 @@ def _add_pos_order_base_lines_vals(self, vals): # Compute values for order lines. In Jordan, because the web-service has absolutely no tolerance, # what we do is: use round per line with 9 decimals (yes!) - base_lines = pos_order._prepare_tax_base_line_values() + base_lines = pos_order.lines._prepare_base_lines_for_taxes_computation() AccountTax = self.env['account.tax'] for base_line in base_lines: diff --git a/addons/l10n_jo_edi_pos/models/pos_order.py b/addons/l10n_jo_edi_pos/models/pos_order.py index 97948e5ef610f5..65c41ecfa67144 100644 --- a/addons/l10n_jo_edi_pos/models/pos_order.py +++ b/addons/l10n_jo_edi_pos/models/pos_order.py @@ -203,9 +203,8 @@ def _prepare_invoice_vals(self): 'preferred_payment_method_line_id': self.env['account.payment.method.line'].search([], limit=1).id, } - def _create_invoice(self, move_vals): - # EXTENDS 'point_of_sale' - invoice = super()._create_invoice(move_vals) + def _generate_pos_order_invoice(self): + invoice = super()._generate_pos_order_invoice() self._link_xml_and_qr_to_invoice(invoice) return invoice diff --git a/addons/l10n_jo_edi_pos/tests/jo_edi_pos_common.py b/addons/l10n_jo_edi_pos/tests/jo_edi_pos_common.py index 0fa06d2fa745ae..1808f5b09de199 100644 --- a/addons/l10n_jo_edi_pos/tests/jo_edi_pos_common.py +++ b/addons/l10n_jo_edi_pos/tests/jo_edi_pos_common.py @@ -54,6 +54,7 @@ def _l10n_jo_create_order_refund(self, order, refund_vals, payments=None, defaul order_line.write(line_write_vals) del refund_vals['lines'] order_refund.write(refund_vals) + order_refund._compute_prices() self._pay_order(order_refund, payments, default_payment) diff --git a/addons/l10n_jo_edi_pos/tests/test_jo_edi_pos_precision.py b/addons/l10n_jo_edi_pos/tests/test_jo_edi_pos_precision.py index bdcb72099e74b9..e751c715542b53 100644 --- a/addons/l10n_jo_edi_pos/tests/test_jo_edi_pos_precision.py +++ b/addons/l10n_jo_edi_pos/tests/test_jo_edi_pos_precision.py @@ -115,21 +115,21 @@ def get_price_units(xml_string): { 'product_id': self.product_b.id, 'price_unit': 11.11, - 'qty': 3.11, + 'qty': -3.11, 'discount': 3.12, 'tax_ids': [Command.set((self.jo_general_tax_16_included).ids)], }, { 'product_id': self.product_a.id, 'price_unit': 10000.01, - 'qty': 2.02, + 'qty': -2.02, 'discount': 99.71, 'tax_ids': [Command.set((self.jo_general_tax_16_included).ids)], }, { 'product_id': self.product_a.id, 'price_unit': 0.01, - 'qty': 0.1, + 'qty': -0.1, 'discount': 2, 'tax_ids': [Command.set((self.jo_general_tax_16_included).ids)], }, diff --git a/addons/l10n_jo_edi_pos/tests/test_jo_edi_pos_types.py b/addons/l10n_jo_edi_pos/tests/test_jo_edi_pos_types.py index aa5c29368f6b88..7064c166034127 100644 --- a/addons/l10n_jo_edi_pos/tests/test_jo_edi_pos_types.py +++ b/addons/l10n_jo_edi_pos/tests/test_jo_edi_pos_types.py @@ -340,19 +340,19 @@ def test_credit_notes_lines_matching_2(self): { # id = 3 'product_id': self.product_a.id, 'price_unit': 10, - 'qty': 2, + 'qty': -2, 'name': '3', # label should not affect matching }, { # id = 1 'product_id': self.product_a.id, 'price_unit': 10, - 'qty': 1, + 'qty': -1, 'name': '1', }, { # id = 2 'product_id': self.product_a.id, 'price_unit': 10, - 'qty': 2, + 'qty': -2, 'name': '2', }, ], @@ -375,28 +375,34 @@ def get_xml_order_type(order): self.company.l10n_jo_edi_taxpayer_type = 'income' self.company.l10n_jo_edi_sequence_income_source = '4419618' - cash_pm1, cash_pm2, bank_pm1, bank_pm2 = self.env['pos.payment.method'].create([ + cash_journal = self.env['account.journal'].create({ + 'name': 'Cash Test', + 'type': 'cash', + 'company_id': self.env.company.id, + 'code': 'MOD', + 'sequence': 10, + }) + cash_pm1, bank_pm1, bank_pm2 = self.env['pos.payment.method'].create([ { 'name': 'Cash 1', - 'l10n_jo_edi_pos_is_cash': True, - }, - { - 'name': 'Cash 2', + 'type': 'cash', + 'journal_id': cash_journal.id, 'l10n_jo_edi_pos_is_cash': True, }, { 'name': 'Bank 1', + 'type': 'bank', 'l10n_jo_edi_pos_is_cash': False, }, { 'name': 'Bank 2', + 'type': 'bank', 'l10n_jo_edi_pos_is_cash': False, }, ]) self.main_pos_config.write({ 'payment_method_ids': [ Command.link(cash_pm1.id), - Command.link(cash_pm2.id), Command.link(bank_pm1.id), Command.link(bank_pm2.id), ], @@ -406,7 +412,7 @@ def get_xml_order_type(order): ([(bank_pm1, 100)], '021'), ([(cash_pm1, 50), (bank_pm1, 50)], False), ([], False), - ([(cash_pm1, 50), (cash_pm2, 50)], '011'), + ([(cash_pm1, 50), (cash_pm1, 50)], '011'), ([(bank_pm1, 50), (bank_pm2, 50)], '021'), ]: order_vals = { @@ -430,13 +436,23 @@ def get_xml_order_type(order): def test_mandatory_customer(self): self.company.l10n_jo_edi_taxpayer_type = 'income' self.company.l10n_jo_edi_sequence_income_source = '4419618' + cash_journal = self.env['account.journal'].create({ + 'name': 'Cash Test', + 'type': 'cash', + 'company_id': self.env.company.id, + 'code': 'MOD', + 'sequence': 10, + }) cash_pm, bank_pm = self.env['pos.payment.method'].create([ { 'name': 'Cash', + 'type': 'cash', + 'journal_id': cash_journal.id, 'l10n_jo_edi_pos_is_cash': True, }, { 'name': 'Bank', + 'type': 'bank', 'l10n_jo_edi_pos_is_cash': False, }, ]) diff --git a/addons/l10n_my_edi_pos/models/account_edi_xml_ubl_my.py b/addons/l10n_my_edi_pos/models/account_edi_xml_ubl_my.py index dce53c966a4395..341c7271161a59 100644 --- a/addons/l10n_my_edi_pos/models/account_edi_xml_ubl_my.py +++ b/addons/l10n_my_edi_pos/models/account_edi_xml_ubl_my.py @@ -126,7 +126,7 @@ def _add_consolidated_invoice_base_lines_vals(self, vals): for index, orders in enumerate(orders_per_line): base_lines = [] for order in orders: - order_base_lines = order._prepare_tax_base_line_values() + order_base_lines = order.lines._prepare_base_lines_for_taxes_computation() AccountTax._add_tax_details_in_base_lines(order_base_lines, consolidated_invoice.company_id) AccountTax._round_base_lines_tax_details(order_base_lines, consolidated_invoice.company_id) base_lines += order_base_lines diff --git a/addons/l10n_my_edi_pos/models/myinvois_document_pos.py b/addons/l10n_my_edi_pos/models/myinvois_document_pos.py index aad5e43cb19a56..1e53b2a3312535 100644 --- a/addons/l10n_my_edi_pos/models/myinvois_document_pos.py +++ b/addons/l10n_my_edi_pos/models/myinvois_document_pos.py @@ -208,7 +208,7 @@ def _get_record_rounded_base_lines(self, record): """ if record and record._name == 'pos.order': AccountTax = self.env["account.tax"] - base_lines = record._prepare_tax_base_line_values() + base_lines = record.lines._prepare_base_lines_for_taxes_computation() AccountTax._add_tax_details_in_base_lines(base_lines, self.company_id) AccountTax._round_base_lines_tax_details(base_lines, self.company_id) return base_lines diff --git a/addons/l10n_my_edi_pos/models/pos_order.py b/addons/l10n_my_edi_pos/models/pos_order.py index 1c7cd68b58f67a..eed3fe816284d0 100644 --- a/addons/l10n_my_edi_pos/models/pos_order.py +++ b/addons/l10n_my_edi_pos/models/pos_order.py @@ -46,7 +46,7 @@ def _process_order(self, order, existing_order): to_invoice = order.get('to_invoice') for refunded_order in refunded_orders: - submitted = ((refunded_order.is_invoiced and refunded_order.account_move.l10n_my_edi_state in ["in_progress", "valid", "rejected"]) + submitted = ((refunded_order.is_singly_invoiced and refunded_order.account_move.l10n_my_edi_state in ["in_progress", "valid", "rejected"]) or (refunded_order._get_active_consolidated_invoice() and refunded_order._get_active_consolidated_invoice().myinvois_state in ["in_progress", "valid", "rejected"])) if submitted and not to_invoice: diff --git a/addons/l10n_my_edi_pos/tests/expected_xmls/consolidated_invoice_refund.xml b/addons/l10n_my_edi_pos/tests/expected_xmls/consolidated_invoice_refund.xml index dcfab12e039037..dbb62f9705ca44 100644 --- a/addons/l10n_my_edi_pos/tests/expected_xmls/consolidated_invoice_refund.xml +++ b/addons/l10n_my_edi_pos/tests/expected_xmls/consolidated_invoice_refund.xml @@ -1,6 +1,6 @@ - RINV/2025/00001 + RPOSS/2025/00001 2025-01-01 00:00:00Z 04 diff --git a/addons/l10n_my_edi_pos/tests/test_myinvois_pos.py b/addons/l10n_my_edi_pos/tests/test_myinvois_pos.py index 1af33db2a07e40..c6a4b0969a6631 100644 --- a/addons/l10n_my_edi_pos/tests/test_myinvois_pos.py +++ b/addons/l10n_my_edi_pos/tests/test_myinvois_pos.py @@ -34,6 +34,7 @@ def setUpClass(cls): }) cash_payment = cls.env['pos.payment.method'].create({ 'name': 'Cash Payment', + 'type': 'cash', 'journal_id': cash_journal.id, 'receivable_account_id': cls.pos_receivable_cash.id, 'company_id': cls.env.company.id, @@ -98,6 +99,7 @@ def setUpClass(cls): }) cash_payment_usd = cls.env['pos.payment.method'].create({ 'name': 'Cash Payment', + 'type': 'cash', 'journal_id': cash_journal_usd.id, 'receivable_account_id': cls.pos_receivable_cash.id, 'company_id': cls.env.company.id, @@ -389,6 +391,7 @@ def test_consolidate_invoices_in_foreign_currency(self): first_order = self._create_order({'pos_order_lines_ui_args': [(self.product_one, 1.0)]}) second_order = self._create_order({'pos_order_lines_ui_args': [(self.product_two, 1.0)]}) # Consolidate them + self.config.journal_id.currency_id = self.env.ref('base.USD') wizard = self.env['myinvois.consolidate.invoice.wizard'].create({ 'date_from': '2025-01-01', 'date_to': '2025-01-31', @@ -586,6 +589,9 @@ def test_refund_order(self): 'refunded_orderline_id': first_order.lines[0].id, }, ], + 'pos_order_ui_args': { + 'is_refund': True, + }, }) wizard = self.env['myinvois.consolidate.invoice.wizard'].create({ 'date_from': '2025-01-01', @@ -617,6 +623,9 @@ def test_refund_order_partially(self): 'refunded_orderline_id': first_order.lines[0].id, }, ], + 'pos_order_ui_args': { + 'is_refund': True, + }, }) wizard = self.env['myinvois.consolidate.invoice.wizard'].create({ 'date_from': '2025-01-01', @@ -722,6 +731,9 @@ def test_refund_constrains_not_submitted(self): # Fails, you shouldn't invoice an order that hasn't been sent to myinvois yet. with self.assertRaises(UserError): self._create_order({ + 'pos_order_ui_args': { + 'is_refund': True, + }, 'pos_order_lines_ui_args': [ { 'product': self.product_one, @@ -731,6 +743,9 @@ def test_refund_constrains_not_submitted(self): ], 'customer': self.invoicing_customer, 'is_invoiced': True, }) self._create_order({ + 'pos_order_ui_args': { + 'is_refund': True, + }, 'pos_order_lines_ui_args': [ { 'product': self.product_one, @@ -818,6 +833,7 @@ def test_consolidate_invoices_export_xml(self): fifth_order = self._create_order({'pos_order_lines_ui_args': [(product_1, 1.0), (product_2, 1.0)]}) # Consolidate them + self.config.journal_id.currency_id = self.env.ref('base.USD') wizard = self.env['myinvois.consolidate.invoice.wizard'].create({ 'date_from': '2025-01-01', 'date_to': '2025-01-31', @@ -869,13 +885,16 @@ def test_consolidate_invoices_refund_export_xml(self): fifth_order = self._create_order({'pos_order_lines_ui_args': [(product_1, 1.0), (product_2, 1.0)]}) # Consolidate them + self.config.journal_id.currency_id = self.env.ref('base.USD') # Crappy patch wizard = self.env['myinvois.consolidate.invoice.wizard'].create({ 'date_from': '2025-01-01', 'date_to': '2025-01-31', 'consolidation_type': 'pos', }) wizard.button_consolidate() - consolidated_invoice = (first_order | second_order | third_order | fourth_order | fifth_order).consolidated_invoice_ids + orders = (first_order | second_order | third_order | fourth_order | fifth_order) + self.assertEqual(orders.currency_id.name, 'USD') + consolidated_invoice = orders.consolidated_invoice_ids # We expect a single invoice self.assertEqual(len(consolidated_invoice), 1) # Add an export custom number; it doesn't make much sense in this flow but supporting it may be useful. @@ -912,7 +931,7 @@ def test_portal_invoice_request_flow(self): "pos_order_lines_ui_args": [(self.product_one, 1.0)], }) - self.assertFalse(order.account_move) + self.assertFalse(order.is_singly_invoiced) url = f"/pos/ticket/validate?access_token={order.access_token}" response = self.url_open(url) # GET request to get csrf token @@ -1002,8 +1021,10 @@ def _mock_successful_submission(self, endpoint, params): def with_pos_session(self): session = self.open_new_session(0.0) yield session - session.post_closing_cash_details(0.0) - session.close_session_from_ui() + cash_pm = self.config._get_cash_payment_method() + session.close_session_from_ui({ + cash_pm.id: 0, + }) def _create_order(self, ui_data): return next(iter(self._create_orders([ui_data]).values())) diff --git a/addons/l10n_my_edi_pos/wizard/myinvois_consolidate_invoice_wizard.py b/addons/l10n_my_edi_pos/wizard/myinvois_consolidate_invoice_wizard.py index 1adec996ed26dd..d32a4a1cfe7aaf 100644 --- a/addons/l10n_my_edi_pos/wizard/myinvois_consolidate_invoice_wizard.py +++ b/addons/l10n_my_edi_pos/wizard/myinvois_consolidate_invoice_wizard.py @@ -34,13 +34,15 @@ def _get_myinvois_document_vals(self): if self.consolidation_type == 'pos': orders_to_consolidate = self.env['pos.order'].search([ ("state", "=", "done"), - ("account_move", "=", False), ('date_order', '>=', self.date_from), ('date_order', '<=', self.date_to), '|', ('consolidated_invoice_ids', '=', False), ('consolidated_invoice_ids', 'not any', [('myinvois_state', '!=', 'cancelled')]) ]) + orders_to_consolidate = orders_to_consolidate.filtered_domain([ + ('is_singly_invoiced', '=', False), + ]) if not orders_to_consolidate: raise ValidationError(self.env._('Invalid Operation. No order to consolidate.')) @@ -56,7 +58,7 @@ def _get_myinvois_document_vals(self): 'company_id': config.company_id.id, 'currency_id': config.currency_id.id, 'pos_config_id': config.id, - 'journal_id': config.invoice_journal_id.id, + 'journal_id': config.journal_id.id, 'is_consolidated_invoice': True, 'move_type': 'out_invoice', # In practice, a consolidated invoice from the PoS can be considered as 'invoice' for the sake of grouping/sequencing }) diff --git a/addons/l10n_pe_pos/views/templates.xml b/addons/l10n_pe_pos/views/templates.xml index 1f959ed25fe09f..665c12b3a352ba 100644 --- a/addons/l10n_pe_pos/views/templates.xml +++ b/addons/l10n_pe_pos/views/templates.xml @@ -8,7 +8,7 @@ -

+

Invoice not available. You can contact us for more info.

diff --git a/addons/l10n_sa_edi_pos/static/tests/tours/test_zatca_pos_invoice.js b/addons/l10n_sa_edi_pos/static/tests/tours/test_zatca_pos_invoice.js index efe6faecb15a9f..44dae311ed78af 100644 --- a/addons/l10n_sa_edi_pos/static/tests/tours/test_zatca_pos_invoice.js +++ b/addons/l10n_sa_edi_pos/static/tests/tours/test_zatca_pos_invoice.js @@ -3,8 +3,6 @@ import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util"; import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util"; import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util"; -import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util"; -import * as PartnerList from "@point_of_sale/../tests/pos/tours/utils/partner_list_util"; import { registry } from "@web/core/registry"; registry.category("web_tour.tours").add("ZATCA_invoice_not_mandatory_if_deposit", { @@ -46,25 +44,3 @@ registry.category("web_tour.tours").add("ZATCA_invoice_mandatory_if_regular_orde PaymentScreen.clickInvoiceButton(), ].flat(), }); - -registry.category("web_tour.tours").add("ZATCA_blocks_settle_due_and_sale_on_same_order", { - steps: () => - [ - Chrome.startPoS(), - Dialog.confirm("Open Register"), - ProductScreen.clickPartnerButton(), - PartnerList.settleCustomerAccount( - "AAAA Generic Partner", - "23.0", - "TSJ/2026/", - "", - false, - false - ), - ProductScreen.clickDisplayedProduct("Whiteboard Pen"), - ProductScreen.clickPayButton(), - PaymentScreen.clickPaymentMethod("Bank"), - PaymentScreen.clickValidate(), - Dialog.is({ title: "Settlement Error" }), - ].flat(), -}); diff --git a/addons/l10n_sa_edi_pos/tests/test_sa_edi_pos.py b/addons/l10n_sa_edi_pos/tests/test_sa_edi_pos.py index 258bd1ddc4db99..476d82b2c8c639 100644 --- a/addons/l10n_sa_edi_pos/tests/test_sa_edi_pos.py +++ b/addons/l10n_sa_edi_pos/tests/test_sa_edi_pos.py @@ -134,69 +134,3 @@ def test_ZATCA_invoice_mandatory_if_regular_order(self): 'ZATCA_invoice_mandatory_if_regular_order', login="pos_admin", ) - - @patch( - "odoo.addons.l10n_sa_edi.models.account_journal.AccountJournal._l10n_sa_ready_to_submit_einvoices", - new=lambda self: True, - ) - def test_ZATCA_blocks_settle_due_and_sale_on_same_order(self): - """ - Tests that the invoice is mandatory in POS payment for ZATCA. - Also is by default checked. - """ - if not self.env["ir.module.module"].search([("name", "=", "pos_settle_due"), ("state", "=", "installed")]): - self.skipTest("pos_settle_due module is required for this test") - - self.customer_account_payment_method = self.env["pos.payment.method"].create( - { - "name": "Customer Account", - "split_transactions": True, - }, - ) - self.main_pos_config.write( - {"payment_method_ids": [(4, self.customer_account_payment_method.id)]}, - ) - - self.assertEqual(self.partner_a.total_due, 0) - - self.main_pos_config.with_user(self.pos_admin).open_ui() - current_session = self.main_pos_config.current_session_id - - order = self.env["pos.order"].create( - { - "company_id": self.env.company.id, - "session_id": current_session.id, - "partner_id": self.partner_a.id, - "lines": [ - Command.create( - { - "product_id": self.whiteboard_pen.product_variant_id.id, - "price_unit": 20, - "discount": 0, - "qty": 1, - "price_subtotal": 20, - "tax_ids": [Command.link(self.tax_sale_a.id)], - "price_subtotal_incl": 23, - }, - ), - ], - "amount_paid": 23, - "amount_total": 23.0, - "amount_tax": 3, - "amount_return": 0.0, - "to_invoice": True, - "last_order_preparation_change": "{}", - }, - ) - - self.make_payment(order, self.customer_account_payment_method, 23) - current_session.action_pos_session_closing_control() - self.assertEqual(self.partner_a.invoices_amount_due, 23) - - self.main_pos_config.open_ui() - - self.start_tour( - "/pos/ui?config_id=%d" % self.main_pos_config.id, - "ZATCA_blocks_settle_due_and_sale_on_same_order", - login="accountman", - ) diff --git a/addons/l10n_test_pos_qr_payment/tests/common.py b/addons/l10n_test_pos_qr_payment/tests/common.py index bab7a362181eab..525a24a012c2dc 100644 --- a/addons/l10n_test_pos_qr_payment/tests/common.py +++ b/addons/l10n_test_pos_qr_payment/tests/common.py @@ -37,6 +37,7 @@ def setUpClass(cls): cls.outstanding_bank = cls.copy_account(cls.inbound_payment_method_line.payment_account_id, {'name': 'Outstanding Bank'}) cls.bank_pm = cls.env['pos.payment.method'].sudo().create({ 'name': 'Bank', + 'type': 'bank', 'journal_id': cls.company_data['default_journal_bank'].id, 'receivable_account_id': cls.pos_receivable_bank.id, 'outstanding_account_id': cls.outstanding_bank.id, diff --git a/addons/l10n_test_pos_qr_payment/tests/test_pos_qr_payment.py b/addons/l10n_test_pos_qr_payment/tests/test_pos_qr_payment.py index 6cf0ec79c3d850..3c1bf85283c738 100644 --- a/addons/l10n_test_pos_qr_payment/tests/test_pos_qr_payment.py +++ b/addons/l10n_test_pos_qr_payment/tests/test_pos_qr_payment.py @@ -25,7 +25,8 @@ def setUpClass(cls): 'name': 'QR Code', 'journal_id': cls.company_data['default_journal_bank'].id, 'payment_method_type': "bank_qr_code", - 'qr_code_method': "sct_qr" + 'qr_code_method': "sct_qr", + 'type': 'bank', }) cls.main_pos_config.write({ 'payment_method_ids': [(4, qr_payment.id)] @@ -87,7 +88,8 @@ def setUpClass(cls): 'name': 'QR Code', 'journal_id': cls.company_data['default_journal_bank'].id, 'payment_method_type': "bank_qr_code", - 'qr_code_method': "ch_qr" + 'qr_code_method': "ch_qr", + 'type': 'bank', }) cls.main_pos_config.write({ 'payment_method_ids': [(4, qr_payment.id)] @@ -147,7 +149,8 @@ def setUpClass(cls): 'name': 'QR Code', 'journal_id': cls.company_data['default_journal_bank'].id, 'payment_method_type': "bank_qr_code", - 'qr_code_method': "emv_qr" + 'qr_code_method': "emv_qr", + 'type': 'bank', }) cls.main_pos_config.write({ 'payment_method_ids': [(4, qr_payment.id)] @@ -202,7 +205,8 @@ def setUpClass(cls): 'name': 'QR Code', 'journal_id': cls.company_data['default_journal_bank'].id, 'payment_method_type': "bank_qr_code", - 'qr_code_method': "emv_qr" + 'qr_code_method': "emv_qr", + 'type': 'bank', }) cls.main_pos_config.write({ 'payment_method_ids': [(4, qr_payment.id)] diff --git a/addons/l10n_vn_edi_viettel_pos/models/pos_order.py b/addons/l10n_vn_edi_viettel_pos/models/pos_order.py index f22d42e51ee722..4b9f1890ca2984 100644 --- a/addons/l10n_vn_edi_viettel_pos/models/pos_order.py +++ b/addons/l10n_vn_edi_viettel_pos/models/pos_order.py @@ -36,11 +36,11 @@ def _prepare_invoice_vals(self): return vals - def _create_invoice(self, move_vals): + def _generate_pos_order_invoice(self): if self.company_id.country_id.code == 'VN' and self.config_id.l10n_vn_auto_send_to_sinvoice: # When auto-sending to SInvoice, we want to skip fetching the SInvoice files # right after sending the invoice to reduce the time spent in the POS checkout flow. # The SInvoice files will be fetched by printing the invoice from the POS order page # or fetched manually in the backend. - return super()._create_invoice(move_vals).with_context(skip_fetch_sinvoice_files=True) - return super()._create_invoice(move_vals) + return super()._generate_pos_order_invoice().with_context(skip_fetch_sinvoice_files=True) + return super()._generate_pos_order_invoice() diff --git a/addons/l10n_vn_edi_viettel_pos/tests/test_l10n_vn_edi_pos.py b/addons/l10n_vn_edi_viettel_pos/tests/test_l10n_vn_edi_pos.py index c0c240a92f36e2..3a86d69e75b4a3 100644 --- a/addons/l10n_vn_edi_viettel_pos/tests/test_l10n_vn_edi_pos.py +++ b/addons/l10n_vn_edi_viettel_pos/tests/test_l10n_vn_edi_pos.py @@ -79,9 +79,7 @@ def test_pos_specific_symbol(self): def test_invoice_send_and_print(self): """ Test the invoice creation, sending and printing from a POS order.""" order = self._create_simple_order() - move_vals = order._prepare_invoice_vals() - invoice = order._create_invoice(move_vals) - invoice.action_post() + invoice = order._generate_pos_order_invoice() self.assertEqual(invoice.l10n_vn_edi_invoice_state, 'ready_to_send') self._send_invoice(invoice) @@ -102,15 +100,11 @@ def test_invoice_send_and_print(self): def test_invoice_refund(self): """ Test the refund flow of PoS order""" order = self._create_simple_order() - move_vals = order._prepare_invoice_vals() - invoice = order._create_invoice(move_vals) - invoice.action_post() + invoice = order._generate_pos_order_invoice() self._send_invoice(invoice) refund_order = order._refund() - refund_move_vals = refund_order._prepare_invoice_vals() - refund_invoice = refund_order._create_invoice(refund_move_vals) - refund_invoice.action_post() + refund_invoice = refund_order._generate_pos_order_invoice() self.assertEqual(refund_invoice.l10n_vn_edi_invoice_state, 'ready_to_send') self._send_invoice(refund_invoice) @@ -130,8 +124,7 @@ def test_invoice_refund(self): def test_fetch_invoice_files(self): """Test that l10n_vn_edi_fetch_invoice_files fetches and stores XML and PDF files on a sent POS invoice.""" order = self._create_simple_order() - invoice = order._create_invoice(order._prepare_invoice_vals()) - invoice.action_post() + invoice = order._generate_pos_order_invoice() self._send_invoice(invoice) self.assertEqual(invoice.l10n_vn_edi_invoice_state, 'sent') @@ -166,8 +159,7 @@ def test_fetch_invoice_files(self): def test_fetch_invoice_files_not_sent_raises(self): """Test that calling l10n_vn_edi_fetch_invoice_files on a non-sent invoice raises a UserError.""" order = self._create_simple_order() - invoice = order._create_invoice(order._prepare_invoice_vals()) - invoice.action_post() + invoice = order._generate_pos_order_invoice() self.assertNotEqual(invoice.l10n_vn_edi_invoice_state, 'sent') with self.assertRaises(UserError): diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 48cadba9b36eef..9c434679543ca0 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -204,7 +204,7 @@ def _parse_additional_values(fields, prefix, kwargs): pos_order = pos_order.with_company(pos_order.company_id).with_context(allowed_company_ids=pos_order.company_id.ids) # If the order was already invoiced, return the invoice directly by forcing the access token so that the non-connected user can see it. - if pos_order.account_move and pos_order.account_move.is_sale_document(): + if pos_order.is_singly_invoiced: return request.redirect('/my/invoices/%s?access_token=%s' % (pos_order.account_move.id, pos_order.account_move._portal_ensure_token())) if not request.env['res.company']._with_locked_records(pos_order, allow_raising=False): diff --git a/addons/point_of_sale/data/orders_demo.xml b/addons/point_of_sale/data/orders_demo.xml index df562097aeadec..4c3ae96ca68c2e 100644 --- a/addons/point_of_sale/data/orders_demo.xml +++ b/addons/point_of_sale/data/orders_demo.xml @@ -90,14 +90,8 @@ 6.78 - - - - - - + @@ -186,15 +180,8 @@ 8.36 - - - - - - @@ -294,15 +281,8 @@ 8.84 - - - - - - @@ -352,13 +332,7 @@ 6.80 - - - - - diff --git a/addons/point_of_sale/data/point_of_sale_data.xml b/addons/point_of_sale/data/point_of_sale_data.xml index 5ad7307cfd681e..de1bdf2db7615c 100644 --- a/addons/point_of_sale/data/point_of_sale_data.xml +++ b/addons/point_of_sale/data/point_of_sale_data.xml @@ -2,6 +2,11 @@ + + Odoo POS + False + + Reload POS Menu diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py index 5f922019d5348c..8b18889d47d383 100644 --- a/addons/point_of_sale/models/__init__.py +++ b/addons/point_of_sale/models/__init__.py @@ -1,6 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import account_bank_statement +from . import account_bank_statement_line from . import pos_load_mixin from . import account_account from . import account_cash_rounding @@ -9,6 +10,7 @@ from . import account_tax from . import account_tax_group from . import account_move +from . import account_move_line from . import pos_bus_mixin from . import barcode_rule from . import binary diff --git a/addons/point_of_sale/models/account_bank_statement.py b/addons/point_of_sale/models/account_bank_statement.py index c25a9b7153f52c..813736157a7219 100644 --- a/addons/point_of_sale/models/account_bank_statement.py +++ b/addons/point_of_sale/models/account_bank_statement.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -# Copyright (C) 2004-2008 PC Solutions (). All Rights Reserved -from odoo import fields, models, api, _ -from odoo.exceptions import UserError +from odoo import fields, models -class AccountBankStatementLine(models.Model): - _inherit = 'account.bank.statement.line' +class AccountBankStatement(models.Model): + _inherit = 'account.bank.statement' - pos_session_id = fields.Many2one('pos.session', string="Session", copy=False, index='btree_not_null') + pos_session_id = fields.One2many( + 'pos.session', + 'bank_statement_id', + string='POS Sessions') diff --git a/addons/point_of_sale/models/account_bank_statement_line.py b/addons/point_of_sale/models/account_bank_statement_line.py new file mode 100644 index 00000000000000..dcc621abad669c --- /dev/null +++ b/addons/point_of_sale/models/account_bank_statement_line.py @@ -0,0 +1,12 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import fields, models + + +class AccountBankStatementLine(models.Model): + _inherit = 'account.bank.statement.line' + + pos_session_id = fields.Many2one( + 'pos.session', + string="Session", + copy=False, + index='btree_not_null') diff --git a/addons/point_of_sale/models/account_cash_rounding.py b/addons/point_of_sale/models/account_cash_rounding.py index 3c84644aaa1bab..95b0ed92b307e1 100644 --- a/addons/point_of_sale/models/account_cash_rounding.py +++ b/addons/point_of_sale/models/account_cash_rounding.py @@ -1,11 +1,27 @@ -from odoo import api, models, _ -from odoo.exceptions import UserError +from odoo import _, api, models +from odoo.exceptions import UserError, ValidationError class AccountCashRounding(models.Model): - _inherit = 'account.cash.rounding' + _name = 'account.cash.rounding' + _inherit = ['account.cash.rounding', 'pos.load.mixin'] @api.ondelete(at_uninstall=False) def _unlink_except_pos_config(self): if self.env['pos.config'].search_count([('rounding_method', 'in', self.ids)], limit=1): raise UserError(_('You cannot delete a rounding method that is used in a Point of Sale configuration.')) + + @api.constrains('rounding', 'rounding_method', 'strategy') + def _check_session_state(self): + open_session = self.env['pos.session'].search([('config_id.rounding_method', 'in', self.ids), ('state', '!=', 'closed')], limit=1) + if open_session: + raise ValidationError( + _("You are not allowed to change the rounding configuration while a pos session using it is already opened.")) + + @api.model + def _load_pos_data_domain(self, data, config): + return [('id', '=', config.rounding_method.id)] + + @api.model + def _load_pos_data_fields(self, config): + return ['id', 'name', 'rounding', 'rounding_method', 'strategy'] diff --git a/addons/point_of_sale/models/account_journal.py b/addons/point_of_sale/models/account_journal.py index 2a2aa00bff2404..88b3dc5409d530 100644 --- a/addons/point_of_sale/models/account_journal.py +++ b/addons/point_of_sale/models/account_journal.py @@ -53,7 +53,11 @@ def _ensure_company_account_journal(self): journal = self.create({ 'name': _('Point of Sale'), 'code': 'POSS', - 'type': 'general', + 'type': 'sale', 'company_id': self.env.company.id, }) + elif journal.type != 'sale': + # Migrate existing POSS journals that were created as 'general' + # before the spec required a sale journal for out_receipt documents. + journal.type = 'sale' return journal diff --git a/addons/point_of_sale/models/account_move.py b/addons/point_of_sale/models/account_move.py index 1c37dc6d3468c1..4b237ac30d5304 100644 --- a/addons/point_of_sale/models/account_move.py +++ b/addons/point_of_sale/models/account_move.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models, api, _ +from odoo import _, api, fields, models class AccountMove(models.Model): @@ -14,9 +12,24 @@ class AccountMove(models.Model): reversed_pos_order_id = fields.Many2one('pos.order', string="Reversed POS Order", index='btree_not_null', help="The pos order that was reverted after closing the session to create an invoice for it.") - pos_session_ids = fields.One2many("pos.session", "move_id", "POS Sessions") + pos_session_ids = fields.One2many("pos.session", compute="_compute_pos_sessions", search="_search_pos_sessions", string="POS Sessions") + pos_session_from_sales_ids = fields.One2many("pos.session", "sales_move_id", "POS Sessions from Sales") + pos_session_from_refunds_ids = fields.One2many("pos.session", "refunds_move_id", "POS Sessions from Refunds") pos_order_count = fields.Integer(compute="_compute_origin_pos_count", string='POS Order Count') + @api.depends('pos_session_from_sales_ids', 'pos_session_from_refunds_ids') + def _compute_pos_sessions(self): + for move in self: + move.pos_session_ids = move.pos_session_from_sales_ids | move.pos_session_from_refunds_ids + + def _search_pos_sessions(self, operator, value): + sessions = self.env['pos.session'].sudo().search([('id', operator, value)]) + return [ + '|', + ('pos_session_from_sales_ids', 'in', sessions.ids), + ('pos_session_from_refunds_ids', 'in', sessions.ids), + ] + @api.depends('pos_order_ids') def _compute_origin_pos_count(self): for move in self: @@ -28,8 +41,6 @@ def _compute_always_tax_exigible(self): # The pos closing move does not create caba entries (anymore); we set the tax values directly on the closing move. # (But there may still be old closing moves that used caba entries from previous versions.) for move in self: - if move.always_tax_exigible or move.tax_cash_basis_created_move_ids: - continue if move.pos_session_ids: move.always_tax_exigible = True @@ -81,7 +92,7 @@ def button_draft(self): @api.model def _load_pos_data_fields(self, config): result = super()._load_pos_data_fields(config) - return result or ['id', 'name'] + return result or ['id', 'name', 'amount_residual', 'move_type'] @api.model def _load_pos_data_domain(self, data, config): diff --git a/addons/point_of_sale/models/account_move_line.py b/addons/point_of_sale/models/account_move_line.py new file mode 100644 index 00000000000000..05cdafa21d6e2b --- /dev/null +++ b/addons/point_of_sale/models/account_move_line.py @@ -0,0 +1,29 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import fields, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + pos_order_line_id = fields.Many2one( + "pos.order.line", + string="POS Order Line", + index="btree_not_null", + help="POS order line that generated this invoice line.", + ) + + def _get_cogs_value(self): + self.ensure_one() + if not self.product_id: + return self.price_unit + price_unit = super()._get_cogs_value() + sudo_order = self.move_id.sudo().pos_order_ids + if sudo_order: + price_unit = sudo_order._get_pos_anglo_saxon_price_unit( + self.product_id, self.quantity, + ) + return price_unit + + def _compute_name(self): + amls = self.filtered(lambda line: not line.move_id.pos_session_ids) + super(AccountMoveLine, amls)._compute_name() diff --git a/addons/point_of_sale/models/account_tax.py b/addons/point_of_sale/models/account_tax.py index 834b1885a914fe..56feead5d54faa 100644 --- a/addons/point_of_sale/models/account_tax.py +++ b/addons/point_of_sale/models/account_tax.py @@ -12,11 +12,13 @@ class AccountTax(models.Model): def write(self, vals): forbidden_fields = { 'amount_type', 'amount', 'type_tax_use', 'tax_group_id', 'price_include', - 'price_include_override', 'include_base_amount', 'is_base_affected', + 'price_include_override', 'include_base_amount', 'is_base_affected', 'active', } if forbidden_fields & set(vals.keys()): lines = self.env['pos.order.line'].sudo().search([ - ('order_id.session_id.state', '!=', 'closed') + '|', ('order_id.session_id.state', '!=', 'closed'), + ('tax_ids', 'in', self.ids), + ('order_id.state', '=', 'draft'), ]) self_ids = set(self.ids) for lines_chunk in map(self.env['pos.order.line'].sudo().browse, split_every(100000, lines.ids)): diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 8e2e6210110543..4bf69cdb4134b3 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -14,8 +14,6 @@ from odoo.tools import SQL, convert from odoo.tools.misc import get_lang -from odoo.addons.point_of_sale.models.pos_printer import format_epson_certified_domain - DEFAULT_LIMIT_LOAD_PRODUCT = 5000 DEFAULT_LIMIT_LOAD_PARTNER = 100 @@ -27,8 +25,7 @@ class PosConfig(models.Model): _check_company_auto = True def _default_sale_journal(self): - journal = self.env['account.journal']._ensure_company_account_journal() - return journal + return self.env['account.journal']._ensure_company_account_journal() def _default_invoice_journal(self): return self.env['account.journal'].search([ @@ -41,13 +38,13 @@ def _default_payment_methods(self): """ domain = [ *self.env['pos.payment.method']._check_company_domain(self.env.company), - ('split_transactions', '=', False), + ('type', 'in', ['cash', 'bank']), '|', ('journal_id', '=', False), ('journal_id.currency_id', 'in', (False, self.env.company.currency_id.id)), ] - non_cash_pm = self.env['pos.payment.method'].search(domain + [('is_cash_count', '=', False)]) - available_cash_pm = self.env['pos.payment.method'].search(domain + [('is_cash_count', '=', True), + non_cash_pm = self.env['pos.payment.method'].search(domain + [('type', '!=', 'cash')]) + available_cash_pm = self.env['pos.payment.method'].search(domain + [('type', '=', 'cash'), ('config_ids', '=', False)], limit=1) if not (non_cash_pm or available_cash_pm): _dummy, payment_methods = self._create_journal_and_payment_methods() @@ -74,17 +71,17 @@ def _get_default_tip_product(self): compute="_compute_is_installed_account_accountant") journal_id = fields.Many2one( 'account.journal', string='Point of Sale Journal', - domain=[('type', 'in', ('general', 'sale'))], + domain=[('type', '=', 'sale')], check_company=True, - help="Accounting journal used to post POS session journal entries and POS invoice payments.", + help="Accounting journal used to post POS session receipts and invoices.", default=_default_sale_journal, ondelete='restrict') - invoice_journal_id = fields.Many2one( - 'account.journal', string='Invoice Journal', - check_company=True, - domain=[('type', '=', 'sale')], - help="Accounting journal used to create invoices.", - default=_default_invoice_journal) + default_partner_id = fields.Many2one( + 'res.partner', + string='Default Customer', + help="The default customer used in PoS session closing", + required=True, + check_company=True) currency_id = fields.Many2one('res.currency', compute='_compute_currency', store=True, compute_sudo=True, string="Currency") order_seq_id = fields.Many2one('ir.sequence', string='Order Sequence', readonly=True, copy=False) order_backend_seq_id = fields.Many2one('ir.sequence', string='Order Backend Sequence', readonly=True, copy=False) @@ -117,7 +114,7 @@ def _get_default_tip_product(self): current_session_id = fields.Many2one('pos.session', compute='_compute_current_session', string="Current Session") current_session_state = fields.Char(compute='_compute_current_session') number_of_rescue_session = fields.Integer(string="Number of Rescue Session", compute='_compute_current_session') - last_session_closing_cash = fields.Float(compute='_compute_last_session') + current_cash_register_balance = fields.Float(compute='_compute_current_cash_register_balance', string="Cash Register") last_session_closing_date = fields.Date(compute='_compute_last_session') pos_session_username = fields.Char(compute='_compute_current_session_user') pos_session_state = fields.Char(compute='_compute_current_session_user') @@ -235,7 +232,7 @@ def _notify_synchronisation(self, session_id, device_identifier, records={}, del 'deleted_record_ids': deleted_record_ids, 'session_id': session_id, 'device_identifier': device_identifier, - 'records': records + 'records': records, }) for config in self.trusted_config_ids: @@ -243,7 +240,7 @@ def _notify_synchronisation(self, session_id, device_identifier, records={}, del 'static_records': static_records, 'session_id': config.current_session_id.id, 'login_number': 0, - 'records': records + 'records': records, }) def read_config_open_orders(self, domain, record_ids=[]): @@ -299,6 +296,10 @@ def _load_pos_data_read(self, records, config): record['_has_cash_delete_perm'] = self.env.user.has_group('account.group_account_basic') record['_pos_special_products_ids'] = self.env['pos.config']._get_special_products().ids + session = config.current_session_id + last_opening = config._get_opening_balance() if session else 0.0 + record['_last_opening_balance'] = last_opening + # Add custom fields for 'formula' taxes. # We can ignore data for _load_pos_data_domain since isn't needed in the domain computation of account.tax taxes = self.env['account.tax'].search(self.env['account.tax']._load_pos_data_domain({}, config)) @@ -329,7 +330,7 @@ def _compute_fast_payment_method_ids(self): @api.depends('payment_method_ids') def _compute_cash_control(self): for config in self: - config.cash_control = bool(config.payment_method_ids.filtered('is_cash_count')) + config.cash_control = bool(config.payment_method_ids.filtered(lambda pm: pm.type == 'cash')) @api.depends('company_id') def _compute_company_has_template(self): @@ -359,9 +360,9 @@ def _compute_current_session(self): rescue_sessions = opened_sessions.filtered('rescue') session = pos_config.session_ids.filtered(lambda s: s.state != 'closed' and not s.rescue) # sessions ordered by id desc - pos_config.has_active_session = opened_sessions and True or False - pos_config.current_session_id = session and session[0].id or False - pos_config.current_session_state = session and session[0].state or False + pos_config.has_active_session = bool(opened_sessions) + pos_config.current_session_id = session[0].id if session else False + pos_config.current_session_state = session[0].state if session else False pos_config.number_of_rescue_session = len(rescue_sessions) def _compute_statistics_for_session(self): @@ -417,10 +418,11 @@ def build_graph_data(date, amount, currency): def get_statistics_for_session(self, session): self.ensure_one() currency = self.currency_id + opening_cash = self._get_opening_balance() statistics = { 'cash': { - 'raw_opening_cash': session.cash_register_balance_start, - 'opening_cash': currency.format(session.cash_register_balance_start) + 'raw_opening_cash': opening_cash, + 'opening_cash': currency.format(opening_cash), }, 'date': { 'is_started': bool(session.start_at), @@ -454,7 +456,7 @@ def get_statistics_for_session(self, session): statistics['orders']['paid'] = { 'amount': total_paid, 'count': paid_order_count, - 'display': f"{currency.format(total_paid)} ({paid_order_count} {'order' if paid_order_count == 1 else 'orders'})" + 'display': f"{currency.format(total_paid)} ({paid_order_count} {'order' if paid_order_count == 1 else 'orders'})", } if draft_orders: @@ -463,7 +465,7 @@ def get_statistics_for_session(self, session): statistics['orders']['draft'] = { 'amount': total_draft, 'count': count_draft, - 'display': f"{currency.format(total_draft)} ({count_draft} {'order' if count_draft == 1 else 'orders'})" + 'display': f"{currency.format(total_draft)} ({count_draft} {'order' if count_draft == 1 else 'orders'})", } return statistics @@ -474,16 +476,44 @@ def _compute_last_session(self): for pos_config in self: session = PosSession.search_read( [('config_id', '=', pos_config.id), ('state', '=', 'closed')], - ['cash_register_balance_end_real', 'stop_at'], + ['stop_at'], order="stop_at desc", limit=1) if session: timezone = self.env.tz pos_config.last_session_closing_date = session[0]['stop_at'].astimezone(timezone).date() - pos_config.last_session_closing_cash = session[0]['cash_register_balance_end_real'] else: - pos_config.last_session_closing_cash = 0 pos_config.last_session_closing_date = False + def action_cash_bank_statement(self): + self.ensure_one() + cash_method = self.payment_method_ids.filtered(lambda pm: pm.type == 'cash') + statement = cash_method.account_bank_statement_id + if not statement: + raise UserError(_("The cash payment method must have a linked bank statement to open the cash control.")) + return { + 'name': _('Cash Control - %s', self.name), + 'type': 'ir.actions.act_window', + 'res_model': 'account.bank.statement', + 'view_mode': 'form', + 'res_id': statement.id, + 'target': 'current', + } + + @api.depends('payment_method_ids.account_bank_statement_id.balance_end') + def _compute_current_cash_register_balance(self): + for pos_config in self: + cash_method = pos_config.payment_method_ids.filtered(lambda pm: pm.type == 'cash') + if len(cash_method) > 1: + raise ValidationError( + _("There should not be more than one cash payment method on a point of sale configuration."), + ) + + if cash_method: + balance = cash_method.journal_id.current_statement_balance + pos_config.current_cash_register_balance = balance + else: + pos_config.current_cash_register_balance = 0 + @api.depends('session_ids') def _compute_current_session_user(self): for pos_config in self: @@ -519,10 +549,10 @@ def _check_rounding_method_strategy(self): def _check_profit_loss_cash_journal(self): if self.cash_control and self.payment_method_ids: for method in self.payment_method_ids: - if method.is_cash_count and (not method.journal_id.loss_account_id or not method.journal_id.profit_account_id): + if method.type == 'cash' and (not method.journal_id.loss_account_id or not method.journal_id.profit_account_id): raise ValidationError(_("You need a loss and profit account on your cash journal.")) - @api.constrains('pricelist_id', 'use_pricelist', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'payment_method_ids') + @api.constrains('pricelist_id', 'use_pricelist', 'available_pricelist_ids', 'journal_id', 'payment_method_ids') def _check_currencies(self): for config in self: if config.use_pricelist and config.pricelist_id and config.pricelist_id not in config.available_pricelist_ids: @@ -537,21 +567,19 @@ def _check_currencies(self): raise ValidationError(_("All available pricelists must be in the same currency as the company or" " as the Sales Journal set on this point of sale if you use" " the Accounting application.")) - if config.invoice_journal_id.currency_id and config.invoice_journal_id.currency_id != config.currency_id: - raise ValidationError(_("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set.")) def _check_payment_method_ids(self): self.ensure_one() if not self.payment_method_ids: raise ValidationError( - _("You must have at least one payment method configured to launch a session.") + _("You must have at least one payment method configured to launch a session."), ) @api.constrains('pricelist_id', 'available_pricelist_ids') def _check_pricelists(self): self._check_companies() - self = self.sudo() - if self.pricelist_id.company_id and self.pricelist_id.company_id != self.company_id: + self_sudo = self.sudo() + if self_sudo.pricelist_id.company_id and self_sudo.pricelist_id.company_id != self_sudo.company_id: raise ValidationError( _("The default pricelist must belong to no company or the company of the point of sale.")) @@ -593,6 +621,23 @@ def _check_company_has_fiscal_country(self): if not self.company_id.account_fiscal_country_id: raise ValidationError(_("The company must have a fiscal country set.")) + def _get_or_create_default_partner(self): + """Get or create the default PoS partner for the current company.""" + partner = self.env.ref('point_of_sale.default_session_closing_partner', raise_if_not_found=False) + default_receivable = self.env.company.account_default_pos_receivable_account_id + + if not partner: + partner = self.env['res.partner'].create({ + 'name': 'Odoo POS', + 'is_company': False, + 'company_id': self.env.company.id, + 'property_account_receivable_id': default_receivable.id, + }) + else: + partner.property_account_receivable_id = default_receivable + + return partner + @api.model_create_multi def create(self, vals_list): for vals in vals_list: @@ -600,6 +645,9 @@ def create(self, vals_list): vals['tip_product_id'] = False vals['set_tip_after_payment'] = False + if not vals.get('default_partner_id', False): + vals['default_partner_id'] = self.sudo()._get_or_create_default_partner().id + self._check_header_footer(vals) pos_configs = super().create(vals_list) @@ -690,7 +738,7 @@ def write(self, vals): if opened_session: forbidden_fields = [] for key in self._get_forbidden_change_fields(): - if key in vals.keys(): + if key in vals: if bypass_payment_method_ids_forbidden_change and key == 'payment_method_ids': continue # Allow activating a pos config even if it has an open session, but don't allow deactivating it. @@ -702,10 +750,10 @@ def write(self, vals): if len(forbidden_fields) > 0: raise UserError(_( "Unable to modify this PoS Configuration because you can't modify %s while a session is open.", - ", ".join(forbidden_fields) + ", ".join(forbidden_fields), )) - result = super(PosConfig, self).write(vals) + result = super().write(vals) for config in self: if config.use_presets and config.default_preset_id and config.default_preset_id.id not in config.available_preset_ids.ids: @@ -764,12 +812,12 @@ def _preprocess_x2many_vals_from_settings_view(self, vals): for command in vals[x2many_field]: if command[0] == 4: - _id = command[1] - if _id in linked_ids: - linked_ids.remove(_id) + id = command[1] + if id in linked_ids: + linked_ids.remove(id) # Remaining items in linked_ids should be unlinked. - unlink_commands = [Command.unlink(_id) for _id in linked_ids] + unlink_commands = [Command.unlink(id) for id in linked_ids] vals[x2many_field] = unlink_commands + vals[x2many_field] @@ -796,7 +844,7 @@ def _get_forbidden_change_fields(self): def unlink(self): # Delete the pos.config records first then delete the sequences linked to them sequences_to_delete = self.order_line_seq_id | self.device_seq_id - res = super(PosConfig, self).unlink() + res = super().unlink() sequences_to_delete.unlink() return res @@ -835,7 +883,6 @@ def _check_groups_implied(self): field_groups = self.env['res.groups'].concat(self.env.ref(it) for it in field_group_xmlids) field_groups.write({'implied_ids': [(4, self.env.ref(field.implied_group).id)]}) - def execute(self): return { 'type': 'ir.actions.client', @@ -938,14 +985,13 @@ def open_opened_rescue_session_form(self): 'res_id': rescue_session_ids.id, 'type': 'ir.actions.act_window', } - else: - return { - 'name': _('Rescue Sessions'), - 'res_model': 'pos.session', - 'view_mode': 'list,form', - 'domain': [('id', 'in', rescue_session_ids.ids)], - 'type': 'ir.actions.act_window', - } + return { + 'name': _('Rescue Sessions'), + 'res_model': 'pos.session', + 'view_mode': 'list,form', + 'domain': [('id', 'in', rescue_session_ids.ids)], + 'type': 'ir.actions.act_window', + } def get_limited_product_count(self): return self.env['ir.config_parameter'].sudo().get_int('point_of_sale.limited_product_count') or DEFAULT_LIMIT_LOAD_PRODUCT @@ -1038,7 +1084,7 @@ def _create_cash_payment_method(self, cash_journal_vals=None): default_cash_account = self.env['account.account'].with_context(lang='en_US').search([ ('account_type', '=', 'asset_cash'), ('name', '=', 'Cash'), - ('company_ids', 'in', self.env.company.root_id.id) + ('company_ids', 'in', self.env.company.root_id.id), ], limit=1) if default_cash_account: @@ -1047,6 +1093,7 @@ def _create_cash_payment_method(self, cash_journal_vals=None): cash_journal = self.env['account.journal'].create(journal_vals) return self.env['pos.payment.method'].create({ 'name': _('Cash'), + 'type': 'cash', 'journal_id': cash_journal.id, 'company_id': self.env.company.id, 'sequence': 1, @@ -1096,6 +1143,7 @@ def _create_journal_and_payment_methods(self, cash_ref=None, cash_journal_vals=N outstanding_account = chart_template.ref('account_journal_payment_debit_account_id', raise_if_not_found=False) or self.env.company.transfer_account_id bank_pm = self.env['pos.payment.method'].create({ 'name': _('Card'), + 'type': 'bank', 'journal_id': bank_journal.id, 'outstanding_account_id': outstanding_account.id if outstanding_account else False, 'company_id': self.env.company.id, @@ -1107,13 +1155,13 @@ def _create_journal_and_payment_methods(self, cash_ref=None, cash_journal_vals=N pay_later_pm = self.env['pos.payment.method'].search([ *self.env['pos.payment.method']._check_company_domain(self.env.company), ('journal_id', '=', False), - ('split_transactions', '=', True), + ('type', '=', 'pay_later'), ]) if not pay_later_pm: pay_later_pm = self.env['pos.payment.method'].create({ 'name': _('Customer Account'), 'company_id': self.env.company.id, - 'split_transactions': True, + 'type': 'pay_later', 'sequence': 4, }) @@ -1126,13 +1174,13 @@ def get_record_by_ref(self, recordRefs): return [self.env.ref(record).id for record in recordRefs if self.env.ref(record, raise_if_not_found=False)] def load_demo_data(self): - self = self.with_context(bypass_categories_forbidden_change=True) - xml_id = self.get_external_id().get(self.id) or self._get_default_demo_data_xml_id() - loaders = self._get_demo_data_loader_methods() + self_ctx = self.with_context(bypass_categories_forbidden_change=True) + xml_id = self_ctx.get_external_id().get(self_ctx.id) or self_ctx._get_default_demo_data_xml_id() + loaders = self_ctx._get_demo_data_loader_methods() for prefix, loader in loaders.items(): if xml_id.startswith(prefix): return loader(True) - return loaders.get(self._get_default_demo_data_xml_id(), self._load_onboarding_furniture_demo_data)(True) + return loaders.get(self_ctx._get_default_demo_data_xml_id(), self_ctx._load_onboarding_furniture_demo_data)(True) def _get_demo_data_loader_methods(self): return { @@ -1152,7 +1200,7 @@ def load_onboarding_clothes_scenario(self, with_demo_data=True): 'name': _('Clothes Shop'), 'company_id': self.env.company.id, 'journal_id': journal.id, - 'payment_method_ids': payment_methods_ids + 'payment_method_ids': payment_methods_ids, }]) self.env['ir.model.data']._update_xmlids([{ 'xml_id': self._get_suffixed_ref_name('point_of_sale.pos_config_clothes'), @@ -1173,7 +1221,7 @@ def _load_onboarding_clothes_demo_data(self, with_demo_data=True): clothes_categories = self.get_record_by_ref([ 'point_of_sale.pos_category_upper', 'point_of_sale.pos_category_lower', - 'point_of_sale.pos_category_others' + 'point_of_sale.pos_category_others', ]) if clothes_categories: self.limit_categories = True @@ -1187,7 +1235,7 @@ def load_onboarding_bakery_scenario(self, with_demo_data=True): 'name': _('Bakery Shop'), 'company_id': self.env.company.id, 'journal_id': journal.id, - 'payment_method_ids': payment_methods_ids + 'payment_method_ids': payment_methods_ids, }) self.env['ir.model.data']._update_xmlids([{ 'xml_id': self._get_suffixed_ref_name('point_of_sale.pos_config_bakery'), @@ -1221,7 +1269,7 @@ def load_onboarding_furniture_scenario(self, with_demo_data=True): 'name': _('Furniture Shop'), 'company_id': self.env.company.id, 'journal_id': journal.id, - 'payment_method_ids': payment_methods_ids + 'payment_method_ids': payment_methods_ids, }]) self.env['ir.model.data']._update_xmlids([{ 'xml_id': self._get_suffixed_ref_name('point_of_sale.pos_config_main'), @@ -1248,7 +1296,7 @@ def _load_onboarding_furniture_demo_data(self, with_demo_data=False): furniture_categories = self.get_record_by_ref([ 'point_of_sale.pos_category_miscellaneous', 'point_of_sale.pos_category_desks', - 'point_of_sale.pos_category_chairs' + 'point_of_sale.pos_category_chairs', ]) if furniture_categories: self.limit_categories = True @@ -1263,7 +1311,7 @@ def load_onboarding_retail_scenario(self, with_demo_data=False): 'name': self.env.company.name, 'company_id': self.env.company.id, 'journal_id': journal.id, - 'payment_method_ids': payment_methods_ids + 'payment_method_ids': payment_methods_ids, }]) self.env['ir.model.data']._update_xmlids([{ 'xml_id': self._get_suffixed_ref_name('point_of_sale.pos_config_retail'), @@ -1277,13 +1325,12 @@ def _get_suffixed_ref_name(self, ref_name): main_company = self.env.ref('base.main_company', raise_if_not_found=False) if main_company and self.env.company.id == main_company.id: return ref_name - else: - return f"{ref_name}_{self.env.company.id}" + return f"{ref_name}_{self.env.company.id}" @api.model def get_pos_kanban_view_state(self): has_pos_config = bool(self.env['pos.config'].search_count( - self._check_company_domain(self.env.company) + self._check_company_domain(self.env.company), )) has_chart_template = bool(self.env.company.chart_template) main_company = self.env.ref('base.main_company', raise_if_not_found=False) @@ -1291,7 +1338,7 @@ def get_pos_kanban_view_state(self): "has_pos_config": has_pos_config, "has_chart_template": has_chart_template, "is_restaurant_installed": bool(self.env['ir.module.module'].search_count([('name', '=', 'pos_restaurant'), ('state', '=', 'installed')])), - "is_main_company": main_company and self.env.company.id == main_company.id or False + "is_main_company": (main_company and self.env.company.id == main_company.id) or False, } @api.model @@ -1321,3 +1368,23 @@ def _set_default_pos_load_limit(self): def _is_quantities_set(self): return self.use_closing_entry_by_product + + def _get_opening_balance(self): + cash_pm = self.sudo()._get_cash_payment_method() + if not cash_pm: + return 0 + amount = self.env['account.bank.statement'].search([ + ('journal_id', '=', cash_pm.journal_id.id), + ], limit=1, order='id desc').balance_end_real + return amount or 0 + + def _get_cash_payment_method(self): + self.ensure_one() + cash_pm = self.payment_method_ids.filtered( + lambda pm: pm.type == 'cash', + ) + if len(cash_pm) > 1: + raise UserError(_( + "There is more than one cash payment method for this PoS Config. Please correct your configuration.", + )) + return cash_pm diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 9a100d3e0ecf68..d75dd7037a5784 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -1,19 +1,25 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -import logging import json -from datetime import datetime +import logging from collections import defaultdict -from uuid import uuid4 -from random import randrange +from datetime import datetime from pprint import pformat +from random import randrange +from uuid import uuid4 from markupsafe import Markup -from odoo import api, fields, models, _ -from odoo.tools import float_is_zero, float_round, float_repr, float_compare, formatLang -from odoo.exceptions import ValidationError, UserError +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError from odoo.fields import Command, Domain - +from odoo.tools import ( + float_compare, + float_is_zero, + float_repr, + float_round, + formatLang, + frozendict, +) _logger = logging.getLogger(__name__) @@ -40,7 +46,7 @@ def _get_valid_session(self, order): open_session = PosSession.search([ ('state', 'not in', ('closed', 'closing_control')), - ('config_id', '=', closed_session.config_id.id) + ('config_id', '=', closed_session.config_id.id), ], limit=1) if open_session: @@ -63,7 +69,7 @@ def _process_order(self, order, existing_order): :returns: id of created/updated pos.order :rtype: int """ - draft = True if order.get('state') == 'draft' else False + draft = order.get('state') == 'draft' pos_session = self.env['pos.session'].browse(order['session_id']) if pos_session.state == 'closing_control' or pos_session.state == 'closed': pos_session = self._get_valid_session(order) @@ -117,13 +123,14 @@ def _process_order(self, order, existing_order): record = self.env[params.comodel_name].search([('uuid', '=', uuids)]) owner_records.filtered(lambda r: r.uuid == uuid).write({name: record.id}) - self = self.with_company(pos_order.company_id) - self._process_payment_lines(order, pos_order, pos_session, draft) + self_comp = self.with_company(pos_order.company_id) + self_comp._process_payment_lines(order, pos_order, pos_session, draft) return pos_order._process_saved_order(draft) def _process_saved_order(self, draft): self.ensure_one() if not draft and self.state != 'cancel': + self._compute_prices() self.action_pos_order_paid() self._set_product_qty_available() self._compute_total_cost_in_real_time() @@ -155,10 +162,13 @@ def _update_lines(self, order, pos_order, fields=[]): order[field] = [] def _generate_order_invoice(self): - if self.to_invoice and self.state == 'paid' and self.config_id.invoice_journal_id: + self.ensure_one() + has_paylater_pm = any(payment.payment_method_id.type == 'pay_later' for payment in self.payment_ids) + if (self.to_invoice or has_paylater_pm) and self.state == 'paid' and self.config_id.journal_id: + self.to_invoice = True # Ensure true if has_paylater_pm is true should_generate_pdf = self.env.context.get('generate_pdf') or self.config_id.use_download_invoice self.with_context(generate_pdf=should_generate_pdf)._generate_pos_order_invoice() - elif not self.config_id.invoice_journal_id: + elif not self.config_id.journal_id: _logger.warning('Trying to create an invoice without any journal configured') raise UserError(_('No invoice journal configured for this POS session.')) @@ -195,14 +205,6 @@ def _process_payment_lines(self, pos_order, order, pos_session, draft): If the payment_line is an updated version of an existing one, the existing payment_line will first be removed before making a new one. - :param pos_order: dictionary representing the order. - :type pos_order: dict. - :param order: Order object the payment lines should belong to. - :type order: pos.order - :param pos_session: PoS session the order was created in. - :type pos_session: pos.session - :param draft: Indicate that the pos_order is not validated yet. - :type draft: bool. """ prec_acc = order.currency_id.decimal_places @@ -210,7 +212,7 @@ def _process_payment_lines(self, pos_order, order, pos_session, draft): order.write({'amount_paid': order._compute_amount_paid()}) if not draft and not float_is_zero(pos_order['amount_return'], prec_acc): - cash_payment_method = pos_session.payment_method_ids.filtered('is_cash_count')[:1] + cash_payment_method = pos_session.payment_method_ids.filtered(lambda pm: pm.type == 'cash')[:1] if not cash_payment_method: raise UserError(_("No cash statement found for this session. Unable to record returned cash.")) return_payment_vals = { @@ -224,85 +226,11 @@ def _process_payment_lines(self, pos_order, order, pos_session, draft): order.add_payment(return_payment_vals) order._compute_prices() - def _prepare_tax_base_line_values(self): - """ Convert pos order lines into dictionaries that would be used to compute taxes later. - - :return: A list of python dictionaries (see '_prepare_base_line_for_taxes_computation' in account.tax). - """ - result = [] - for order in self: - result.extend(order.lines._prepare_tax_base_line_values() or []) - return result - - @api.model - def _get_invoice_lines_values(self, line_values, pos_line, move_type): - # correct quantity sign based on move type and if line is refund. - is_refund_order = bool( - pos_line.order_id.is_refund - or pos_line.order_id.amount_total < 0.0 - ) - qty_sign = -1 if ( - (move_type == 'out_invoice' and is_refund_order) - or (move_type == 'out_refund' and not is_refund_order) - ) else 1 - - if line_values['product_id'].type == 'combo': - quantity = int(line_values['quantity']) if line_values['quantity'] == int( - line_values['quantity']) else line_values['quantity'] - return { - 'display_type': 'line_section', - 'name': f"{line_values['product_id'].name} x {quantity}", - 'quantity': qty_sign * line_values['quantity'], - 'product_uom_id': line_values['uom_id'].id, - } - - return { - 'product_id': line_values['product_id'].id, - 'quantity': qty_sign * line_values['quantity'], - 'discount': line_values['discount'], - 'price_unit': line_values['price_unit'], - 'name': line_values['name'], - 'tax_ids': [(6, 0, line_values['tax_ids'].ids)], - 'product_uom_id': line_values['uom_id'].id, - 'extra_tax_data': self.env['account.tax']._export_base_line_extra_tax_data(line_values), - } - - def _prepare_invoice_lines(self, move_type): - """ Prepare a list of orm commands containing the dictionaries to fill the - 'invoice_line_ids' field when creating an invoice. - - :return: A list of Command.create to fill 'invoice_line_ids' when calling account.move.create. - """ - invoice_lines = [] - for order in self: - line_values_list = order.with_context(invoicing=True)._prepare_tax_base_line_values() - for line_values in line_values_list: - line = line_values['record'] - invoice_lines_values = order._get_invoice_lines_values(line_values, line, move_type) - invoice_lines.append((0, None, invoice_lines_values)) - - is_percentage = order.pricelist_id and any( - order.pricelist_id.item_ids.filtered( - lambda rule: rule.compute_price == "percentage") - ) - if is_percentage and float_compare(line.price_unit, line.product_id.lst_price, precision_rounding=order.currency_id.rounding) < 0: - invoice_lines.append((0, None, { - 'name': _('Price discount from %(original_price)s to %(discounted_price)s', - original_price=float_repr(line.product_id.lst_price, order.currency_id.decimal_places), - discounted_price=float_repr(line.price_unit, order.currency_id.decimal_places)), - 'display_type': 'line_note', - })) - if line.customer_note: - invoice_lines.append((0, None, { - 'name': line.customer_note, - 'display_type': 'line_note', - })) - if order.general_customer_note: - invoice_lines.append((0, None, { - 'name': order.general_customer_note, - 'display_type': 'line_note', - })) - return invoice_lines + def _get_pos_anglo_saxon_price_unit(self, product, quantity): + moves = self.mapped('picking_ids.move_ids')\ + .filtered(lambda m: m.is_valued and m.product_id.valuation == 'real_time' and m.product_id.id == product.id)\ + .sorted(lambda x: x.date) + return moves._get_price_unit() name = fields.Char(string='Order Ref', required=True, readonly=True, copy=False, default='/') last_order_preparation_change = fields.Char(string='Last preparation change', help="Last printed state of the order") @@ -352,10 +280,10 @@ def _prepare_invoice_lines(self, move_type): readonly=False, ) payment_ids = fields.One2many('pos.payment', 'pos_order_id', string='Payments') - session_move_id = fields.Many2one('account.move', string='Session Journal Entry', related='session_id.move_id', readonly=True, copy=False) to_invoice = fields.Boolean('To invoice', copy=False) preset_time = fields.Datetime(string='Hour', help="Hour of the day for the order") - is_invoiced = fields.Boolean('Is Invoiced', compute='_compute_is_invoiced') + is_singly_invoiced = fields.Boolean('Is Singly Invoiced', compute='_compute_is_invoiced') + is_globally_invoiced = fields.Boolean('Is Globally Invoiced', compute='_compute_is_invoiced') is_tipped = fields.Boolean('Is this already tipped?', readonly=True) tip_amount = fields.Monetary(string='Tip Amount', readonly=True) refund_orders_count = fields.Integer('Number of Refund Orders', compute='_compute_refund_related_fields', help="Number of orders where items from this order were refunded") @@ -373,12 +301,12 @@ def _prepare_invoice_lines(self, move_type): invoice_status = fields.Selection([ ('invoiced', 'Fully Invoiced'), ('to_invoice', 'To Invoice'), - ], string='Invoice Status', compute='_compute_invoice_status') + ], string='Invoice Status', compute='_compute_is_invoiced') reversed_move_ids = fields.One2many( 'account.move', 'reversed_pos_order_id', string="Reversal Journal Entries", - help="List of journal entries created when this POS order was reversed and invoiced after session close." + help="List of journal entries created when this POS order was reversed and invoiced after session close.", ) source = fields.Selection(string="Origin", selection=[('pos', 'Point of Sale')], default='pos') defer_invoice_pdf = fields.Boolean(string="Defer Invoice PDF Generation", index=True) @@ -416,11 +344,6 @@ def _ensure_to_keep_last_preparation_change(self, vals): local_change['metadata']['serverDate'] = fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S') vals['last_order_preparation_change'] = json.dumps(local_change) - @api.depends('account_move') - def _compute_invoice_status(self): - for order in self: - order.invoice_status = 'invoiced' if len(order.account_move) else 'to_invoice' - @api.depends('session_id') def _compute_order_config_id(self): for order in self: @@ -437,12 +360,16 @@ def _compute_refund_related_fields(self): def _compute_has_refundable_lines(self): digits = self.env['decimal.precision'].precision_get('Product Unit') for order in self: - order.has_refundable_lines = any([float_compare(line.qty, line.refunded_qty, digits) > 0 for line in order.lines]) + order.has_refundable_lines = any(float_compare(line.qty, line.refunded_qty, digits) > 0 for line in order.lines) - @api.depends('account_move') + @api.depends('account_move', 'session_id.move_ids') def _compute_is_invoiced(self): for order in self: - order.is_invoiced = bool(order.account_move) + order_move = order.account_move + session_moves = order.session_id.move_ids + order.is_singly_invoiced = bool(order_move and order_move not in session_moves) + order.is_globally_invoiced = bool(order_move and order_move in session_moves) + order.invoice_status = 'invoiced' if order.is_singly_invoiced else 'to_invoice' @api.depends('date_order', 'company_id', 'currency_id', 'company_id.currency_id') def _compute_currency_rate(self): @@ -482,8 +409,8 @@ def _compute_margin(self): if order.is_total_cost_computed: order.margin = sum(order.lines.mapped('margin')) amount_untaxed = order.currency_id.round(sum(line.price_subtotal for line in order.lines)) * sign - order.margin_percent = not float_is_zero(amount_untaxed, precision_rounding=order.currency_id.rounding) \ - and order.margin / amount_untaxed \ + order.margin_percent = (not float_is_zero(amount_untaxed, precision_rounding=order.currency_id.rounding) + and order.margin / amount_untaxed) \ or 0 else: order.margin = 0 @@ -496,17 +423,20 @@ def _onchange_amount_all(self): def _get_order_tax_totals(self): self.ensure_one() self.amount_paid = sum(payment.amount for payment in self.payment_ids) - self.amount_return = -sum(payment.amount for payment in self.payment_ids if payment.amount < 0 and not self.is_refund) - base_lines = self.lines._prepare_tax_base_line_values() + self.amount_return = -sum((payment.amount < 0 and payment.amount) or 0 for payment in self.payment_ids) + base_lines = self.lines._prepare_base_lines_for_taxes_computation() self.env['account.tax']._add_tax_details_in_base_lines(base_lines, self.company_id) self.env['account.tax']._round_base_lines_tax_details(base_lines, self.company_id) cash_rounding = None - if ( - self.config_id.cash_rounding - and not self.config_id.only_round_cash_method - and self.config_id.rounding_method - ): + use_rounding = None + only_cash = self.config_id.only_round_cash_method + available_type = ['cash'] if only_cash else ['cash', 'bank'] + use_rounding = self.payment_ids.filtered_domain([ + ('payment_method_id.type', 'in', available_type), + ]) + + if use_rounding: cash_rounding = self.config_id.rounding_method return self.env['account.tax']._get_tax_totals_summary( @@ -521,10 +451,12 @@ def _compute_prices(self): if not order.currency_id: raise UserError(_("You can't: create a pos order from the backend interface, or unset the pricelist, or create a pos.order in a python test with Form tool, or edit the form view in studio if no PoS order exist")) tax_totals = order._get_order_tax_totals() - refund_factor = -1 if (order.is_refund or order.amount_total < 0.0) else 1 + rounding_base_amount_currency = tax_totals.get('cash_rounding_base_amount_currency', 0) + amount_total = tax_totals['total_amount_currency'] - rounding_base_amount_currency + refund_factor = -1 if order.is_refund else 1 order.amount_tax = refund_factor * tax_totals['tax_amount_currency'] - order.amount_total = refund_factor * tax_totals['total_amount_currency'] - order.amount_difference = order.amount_paid - order.amount_total + order.amount_total = refund_factor * amount_total + order.amount_difference = order.amount_paid - amount_total @api.depends('lines.is_edited', 'has_deleted_line') def _compute_is_edited(self): @@ -604,7 +536,7 @@ def write(self, vals): totally_paid_or_more = order.currency_id.compare_amounts(order.amount_paid, order.amount_total) if totally_paid_or_more < 0 and order.state in ['paid', 'done']: raise UserError(_('The paid amount is different from the total amount of the order.')) - elif totally_paid_or_more > 0 and order.state == 'paid': + if totally_paid_or_more > 0 and order.state == 'paid': list_line.append(_("Warning, the paid amount is higher than the total amount. (Difference: %s)", formatLang(self.env, order.amount_paid - order.amount_total, currency_obj=order.currency_id))) if order.nb_print > 0 and any(command[0] in [0, 1] and command[2].get('payment_status') and command[2]['payment_status'] != 'cancelled' for command in vals.get('payment_ids')): raise UserError(_('You cannot change the payment of a printed order.')) @@ -683,11 +615,10 @@ def _compute_order_name(self, session=None): session = session or self.session_id if self.refunded_order_id.exists(): return _('%(refunded_order)s REFUND', refunded_order=self.refunded_order_id.name) - else: - last_reference_part = self.get_reference_last_part() - prefix = session.config_id.order_seq_id.prefix or session.config_id.name - suffix = f" - {session.config_id.order_seq_id.suffix}" if session.config_id.order_seq_id.suffix else '' - return f"{prefix} - {last_reference_part}{suffix}" + last_reference_part = self.get_reference_last_part() + prefix = session.config_id.order_seq_id.prefix or session.config_id.name + suffix = f" - {session.config_id.order_seq_id.suffix}" if session.config_id.order_seq_id.suffix else '' + return f"{prefix} - {last_reference_part}{suffix}" def get_reference_last_part(self): return self.pos_reference.split('-')[-1] @@ -728,14 +659,13 @@ def action_view_invoice(self): 'type': 'ir.actions.act_window', 'res_id': self.account_move.id, } - else: - return { - 'name': _('Customer Invoices'), - 'view_mode': 'list,form', - 'res_model': 'account.move', - 'type': 'ir.actions.act_window', - 'domain': [('id', 'in', invoices.ids)], - } + return { + 'name': _('Customer Invoices'), + 'view_mode': 'list,form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'domain': [('id', 'in', invoices.ids)], + } def action_create_invoices(self): return { @@ -745,7 +675,7 @@ def action_create_invoices(self): 'res_model': 'pos.make.invoice', 'target': 'new', 'type': 'ir.actions.act_window', - 'context': {'dialog_size': 'medium'} + 'context': {'dialog_size': 'medium'}, } # the refunded order is the order from which the items were refunded in this order @@ -780,8 +710,8 @@ def _is_pos_order_paid(self): def _get_rounded_amount(self, amount, force_round=False): # TODO: add support for mix of cash and non-cash payments when both cash_rounding and only_round_cash_method are True if self.config_id.cash_rounding \ - and (force_round or (not self.config_id.only_round_cash_method \ - or any(p.payment_method_id.is_cash_count for p in self.payment_ids))): + and (force_round or (not self.config_id.only_round_cash_method + or any(p.payment_method_id.type == 'cash' for p in self.payment_ids))): amount = float_round(amount, precision_rounding=self.config_id.rounding_method.rounding, rounding_method=self.config_id.rounding_method.rounding_method) currency = self.currency_id return currency.round(amount) if currency else amount @@ -809,66 +739,13 @@ def _first_allowed(bank_ids): return partner_bank_id.id if partner_bank_id else False - def _create_invoice(self, move_vals): - AccountMove = self.env['account.move'] - - invoice = AccountMove.sudo()\ - .with_company(self.company_id)\ - .with_context(default_move_type=move_vals['move_type'], linked_to_pos=True)\ - .create(move_vals) - currency = self.currency_id - amount_total = sum(order.amount_total for order in self) - payment_total = sum(order.amount_paid for order in self) - - if self.config_id.cash_rounding and invoice.invoice_cash_rounding_id: - line_ids_commands = [] - rate = invoice.invoice_currency_rate - sign = invoice.direction_sign - amount_paid = (-1 if amount_total < 0.0 else 1) * payment_total - difference_currency = sign * (amount_paid - invoice.amount_total) - difference_balance = invoice.company_currency_id.round(difference_currency / rate) if rate else 0.0 - if not currency.is_zero(difference_currency): - rounding_line = invoice.line_ids.filtered(lambda line: line.display_type == 'rounding' and not line.tax_line_id) - if rounding_line: - line_ids_commands.append(Command.update(rounding_line.id, { - 'amount_currency': rounding_line.amount_currency + difference_currency, - 'balance': rounding_line.balance + difference_balance, - })) - else: - if difference_currency > 0.0: - account = invoice.invoice_cash_rounding_id.loss_account_id - else: - account = invoice.invoice_cash_rounding_id.profit_account_id - line_ids_commands.append(Command.create({ - 'name': invoice.invoice_cash_rounding_id.name, - 'amount_currency': difference_currency, - 'balance': difference_balance, - 'currency_id': invoice.currency_id.id, - 'display_type': 'rounding', - 'account_id': account.id, - })) - existing_terms_line = invoice.line_ids\ - .filtered(lambda line: line.display_type == 'payment_term')\ - .sorted(lambda line: -abs(line.amount_currency))[:1] - line_ids_commands.append(Command.update(existing_terms_line.id, { - 'amount_currency': existing_terms_line.amount_currency - difference_currency, - 'balance': existing_terms_line.balance - difference_balance, - })) - with AccountMove._check_balanced({'records': invoice}): - invoice.with_context(skip_invoice_sync=True).line_ids = line_ids_commands - body = _("This invoice has been created from the point of sale session:%s", - Markup().join(Markup("%s ") % order._get_html_link() for order in self) - ) - invoice.message_post(body=body) - return invoice - def action_pos_order_paid(self): self.ensure_one() # TODO: add support for mix of cash and non-cash payments when both cash_rounding and only_round_cash_method are True if not self.config_id.cash_rounding \ - or self.config_id.only_round_cash_method \ - and not any(p.payment_method_id.is_cash_count for p in self.payment_ids): + or (self.config_id.only_round_cash_method + and not any(p.payment_method_id.type == 'cash' for p in self.payment_ids)): total = self.amount_total else: total = float_round(self.amount_total, precision_rounding=self.config_id.rounding_method.rounding, rounding_method=self.config_id.rounding_method.rounding_method) @@ -877,7 +754,7 @@ def action_pos_order_paid(self): if not isPaid and not self.config_id.cash_rounding: raise UserError(_("Order %s is not fully paid.", self.name)) - elif not isPaid and self.config_id.cash_rounding: + if not isPaid and self.config_id.cash_rounding: currency = self.currency_id if self.config_id.rounding_method.rounding_method == "HALF-UP": maxDiff = currency.round(self.config_id.rounding_method.rounding / 2) @@ -892,62 +769,6 @@ def action_pos_order_paid(self): return True - def _prepare_invoice_vals(self): - """We have orders filtered by company > config > partners > fiscal_positions so it won't make any issue - when we access user, partner, bank or similar directly. - """ - timezone = self.env.tz - invoice_date = fields.Datetime.now() - is_single_order = len(self) == 1 - - if is_single_order and self.session_id.state != 'closed': - invoice_date = self.date_order - - pos_refunded_invoice_ids = [] - for orderline in self.lines: - if orderline.refunded_orderline_id and orderline.refunded_orderline_id.order_id.account_move: - pos_refunded_invoice_ids.append(orderline.refunded_orderline_id.order_id.account_move.id) - - fiscal_position = self.fiscal_position_id - pos_config = self.config_id - move_type = 'out_invoice' if not any( - order.is_refund or order.amount_total < 0.0 for order in self - ) else 'out_refund' - invoice_payment_term_id = ( - self.partner_id.property_payment_term_id.id - if self.partner_id.property_payment_term_id and any(p.payment_method_id.type == 'pay_later' for p in self.payment_ids) - else False - ) - - vals = { - 'invoice_origin': ', '.join(ref or '' for ref in self.mapped('pos_reference')), - 'pos_refunded_invoice_ids': pos_refunded_invoice_ids, - 'pos_order_ids': self.ids, - 'ref': self.name if is_single_order else False, - 'journal_id': self.config_id.invoice_journal_id.id, - 'move_type': move_type, - 'partner_id': self.partner_id.address_get(['invoice'])['invoice'], - 'partner_shipping_id': self.partner_id.address_get(['delivery'])['delivery'], - 'partner_bank_id': self._get_partner_bank_id(), - 'currency_id': self.currency_id.id, - 'invoice_date': invoice_date.astimezone(timezone).date(), - 'invoice_user_id': self.user_id.id, - 'fiscal_position_id': fiscal_position.id, - 'invoice_line_ids': self._prepare_invoice_lines(move_type), - 'invoice_payment_term_id': invoice_payment_term_id, - } - if is_single_order and self.refunded_order_id.account_move: - vals['ref'] = _('Reversal of: %s', self.refunded_order_id.account_move.name) - vals['reversed_entry_id'] = self.refunded_order_id.account_move.id - - if pos_config.cash_rounding and (not pos_config.only_round_cash_method or any(p.payment_method_id.is_cash_count for p in self.payment_ids)): - vals['invoice_cash_rounding_id'] = pos_config.rounding_method.id - - if any(order.floating_order_name for order in self): - vals.update({'narration': ', '.join(self.filtered('floating_order_name').mapped('floating_order_name'))}) - - return vals - def _prepare_product_aml_dict(self, base_line_vals, update_base_line_vals, rate, sign): amount_currency = update_base_line_vals['amount_currency'] balance = self.company_id.currency_id.round(amount_currency * rate) @@ -966,155 +787,6 @@ def _prepare_product_aml_dict(self, base_line_vals, update_base_line_vals, rate, 'no_followup': False, } - def _prepare_aml_values_list_per_nature(self): - AccountTax = self.env['account.tax'] - sign = 1 if self.amount_total < 0 else -1 - commercial_partner = self.partner_id.commercial_partner_id - company_currency = self.company_id.currency_id - rate = self.currency_id._get_conversion_rate(self.currency_id, company_currency, self.company_id, self.date_order) - - # Concert each order line to a dictionary containing business values. Also, prepare for taxes computation. - base_lines = self._prepare_tax_base_line_values() - AccountTax._add_tax_details_in_base_lines(base_lines, self.company_id) - AccountTax._round_base_lines_tax_details(base_lines, self.company_id) - AccountTax._add_accounting_data_in_base_lines_tax_details(base_lines, self.company_id) - tax_results = AccountTax._prepare_tax_lines(base_lines, self.company_id) - - total_balance = 0.0 - total_amount_currency = 0.0 - aml_vals_list_per_nature = defaultdict(list) - - # Create the tax lines - for tax_line in tax_results['tax_lines_to_add']: - aml_vals_list_per_nature['tax'].append({ - **tax_line, - 'display_type': 'tax', - }) - total_amount_currency += tax_line['amount_currency'] - total_balance += tax_line['balance'] - - # Create the aml values for order lines. - for base_line_vals, update_base_line_vals in tax_results['base_lines_to_update']: - product_dict = self._prepare_product_aml_dict(base_line_vals, update_base_line_vals, rate, sign) - aml_vals_list_per_nature['product'].append(product_dict) - total_amount_currency += product_dict['amount_currency'] - total_balance += product_dict['balance'] - - # Cash rounding. - cash_rounding = self.config_id.rounding_method - if self.config_id.cash_rounding and cash_rounding and (not self.config_id.only_round_cash_method or any(p.payment_method_id.is_cash_count for p in self.payment_ids)): - if self.config_id.only_round_cash_method and any(not p.payment_method_id.is_cash_count for p in self.payment_ids): - # If only_round_cash_method is True, and there are non-cash payments, cash rounding must be computed - # based on the total amount of the order, and total payment amount. - total_payment_amount = self.currency_id.round(sum(p.amount for p in self.payment_ids)) - amount_currency = sign * self.currency_id.round(self.currency_id.round(total_amount_currency) + total_payment_amount) - else: - amount_currency = cash_rounding.compute_difference(self.currency_id, total_amount_currency) - if not self.currency_id.is_zero(amount_currency): - balance = company_currency.round(amount_currency * rate) - - if cash_rounding.strategy == 'biggest_tax': - biggest_tax_aml_vals = None - for aml_vals in aml_vals_list_per_nature['tax']: - if not biggest_tax_aml_vals or float_compare(-sign * aml_vals['amount_currency'], -sign * biggest_tax_aml_vals['amount_currency'], precision_rounding=self.currency_id.rounding) > 0: - biggest_tax_aml_vals = aml_vals - if biggest_tax_aml_vals: - biggest_tax_aml_vals['amount_currency'] += amount_currency - biggest_tax_aml_vals['balance'] += balance - elif cash_rounding.strategy == 'add_invoice_line': - if -sign * amount_currency > 0.0 and cash_rounding.loss_account_id: - account_id = cash_rounding.loss_account_id.id - else: - account_id = cash_rounding.profit_account_id.id - aml_vals_list_per_nature['cash_rounding'].append({ - 'name': cash_rounding.name, - 'account_id': account_id, - 'partner_id': commercial_partner.id, - 'currency_id': self.currency_id.id, - 'amount_currency': amount_currency, - 'balance': balance, - 'display_type': 'rounding', - }) - - # sort self.payment_ids by is_split_transaction: - for payment_id in self.payment_ids: - is_split_transaction = payment_id.payment_method_id.split_transactions - if is_split_transaction: - reversed_move_receivable_account_id = self.partner_id.property_account_receivable_id - else: - reversed_move_receivable_account_id = payment_id.payment_method_id.receivable_account_id or self.company_id.account_default_pos_receivable_account_id - - aml_vals_entry_found = [aml_entry for aml_entry in aml_vals_list_per_nature['payment_terms'] - if aml_entry['account_id'] == reversed_move_receivable_account_id.id - and not aml_entry['partner_id']] - - if aml_vals_entry_found and not is_split_transaction: - aml_vals_entry_found[0]['amount_currency'] += self.session_id._amount_converter(payment_id.amount, self.date_order, False) - aml_vals_entry_found[0]['balance'] += payment_id.amount - else: - aml_vals_list_per_nature['payment_terms'].append({ - 'partner_id': commercial_partner.id if is_split_transaction else False, - 'name': f"{reversed_move_receivable_account_id.code} {reversed_move_receivable_account_id.code}", - 'account_id': reversed_move_receivable_account_id.id, - 'currency_id': self.currency_id.id, - 'amount_currency': payment_id.amount, - 'balance': self.session_id._amount_converter(payment_id.amount, self.date_order, False), - 'display_type': 'payment_term', - }) - - return aml_vals_list_per_nature - - def _create_misc_reversal_move(self, payment_moves): - """ Create a misc move to reverse POS orders and "remove" it from the POS closing entry. - This is done by taking data from the orders and using it to somewhat replicate the resulting entry in orders to - reverse partially the movements done in the POS closing entry. - """ - self.ensure_one() - aml_values_list_per_nature = self._prepare_aml_values_list_per_nature() - move_lines = [] - for aml_values_list in aml_values_list_per_nature.values(): - for aml_values in aml_values_list: - aml_values['balance'] = -aml_values['balance'] - aml_values['amount_currency'] = -aml_values['amount_currency'] - move_lines.append(aml_values) - - # Make a move with all the lines. - reversal_entry = self.env['account.move'].with_context( - default_journal_id=self.config_id.journal_id.id, - skip_invoice_sync=True, - skip_invoice_line_sync=True, - ).create({ - 'journal_id': self.config_id.journal_id.id, - 'date': fields.Date.context_today(self), - 'ref': _('Reversal of POS closing entry %(entry)s for order %(order)s from session %(session)s', entry=self.session_move_id.name, order=self.name, session=self.session_id.name), - 'line_ids': [(0, 0, aml_value) for aml_value in move_lines], - 'reversed_pos_order_id': self.id - }) - reversal_entry.action_post() - - partner = self.partner_id.commercial_partner_id - accounts = ( - self.company_id.account_default_pos_receivable_account_id | - self.payment_ids.mapped('payment_method_id.receivable_account_id') | - partner.property_account_receivable_id - ) - - candidate_lines = reversal_entry.line_ids - if payment_moves.line_ids: - candidate_lines |= payment_moves.line_ids - else: - candidate_lines |= self.session_move_id.line_ids.filtered( - lambda l: l.partner_id == partner and l.account_id == partner.property_account_receivable_id - ) - - lines_by_account = {} - for line in candidate_lines: - if line.account_id in accounts and not line.reconciled: - lines_by_account.setdefault(line.account_id, self.env['account.move.line']) - lines_by_account[line.account_id] |= line - for lines in lines_by_account.values(): - lines.reconcile() - @api.model def get_example_order_data(self): last_order = self.env['pos.order'].search([], order='id desc', limit=1) @@ -1128,79 +800,12 @@ def action_pos_order_receipt(self): "target": "new", } - def action_invoice_download_pdf(self): - self.ensure_one() - if self.defer_invoice_pdf: - self.account_move.with_context(skip_invoice_sync=True)._generate_and_send() - self.defer_invoice_pdf = False - return self.account_move.action_invoice_download_pdf() - - def action_pos_order_invoice(self): - self.ensure_one() - if not (move := self.account_move): - move = self._prepare_missing_invoice_moves() - return { - 'name': _('Customer Invoice'), - 'view_mode': 'form', - 'view_id': self.env.ref('account.view_move_form').id, - 'res_model': 'account.move', - 'context': "{'move_type':'out_invoice'}", - 'type': 'ir.actions.act_window', - 'target': 'current', - 'res_id': move.id, - } - - def _prepare_missing_invoice_moves(self): - self.write({'to_invoice': True}) - return self._generate_pos_order_invoice() - def _get_invoice_post_context(self): return {"skip_invoice_sync": True} def _get_payments(self): return self.payment_ids.sudo().with_company(self.company_id) - def _generate_pos_order_invoice(self): - if not self.env['res.company']._with_locked_records(self, allow_raising=False): - raise UserError(_("Some orders are already being invoiced. Please try again later.")) - self.state = 'done' - - company = self.company_id - invoice_vals = self._prepare_invoice_vals() - invoice = self._create_invoice(invoice_vals) - invoice.sudo().with_company(company).with_context(**self._get_invoice_post_context())._post() - - # invoice payments - payment_moves_from_closed_sessions = {} - all_payment_moves = self.env['account.move'] - for session, orders in self.grouped('session_id').items(): - is_session_closed = session.state == 'closed' - for order in orders: - order_payments = order._get_payments() - payment_moves = order_payments._create_payment_moves(is_session_closed) - all_payment_moves |= payment_moves - if is_session_closed: - payment_moves_from_closed_sessions[order] = payment_moves - - self._reconcile_invoice_payments(invoice, all_payment_moves) - - # reverse payment moves from closed sessions - for order, payment_moves in payment_moves_from_closed_sessions.items(): - order._create_misc_reversal_move(payment_moves) - - if self.env.context.get('generate_pdf', True): - invoice.with_context(skip_invoice_sync=True)._generate_and_send() - else: - order.defer_invoice_pdf = True - - return invoice - - def _reconcile_invoice_payments(self, invoice, payment_moves): - receivable_account = self.env["res.partner"]._find_accounting_partner(invoice.partner_id).with_company(self.company_id).property_account_receivable_id - payment_receivable_lines = payment_moves.pos_payment_ids._get_receivable_lines_for_invoice_reconciliation(receivable_account) - invoice_receivable_lines = invoice.line_ids.filtered(lambda line: line.account_id == receivable_account and not line.reconciled) - (payment_receivable_lines | invoice_receivable_lines).sudo().with_company(invoice.company_id).reconcile() - def cancel_order_from_pos(self): draft_orders = self.filtered(lambda o: o.state == 'draft') if self.env.context.get('active_ids'): @@ -1217,7 +822,7 @@ def cancel_order_from_pos(self): config.notify_synchronisation(config.current_session_id.id, self.env.context.get('device_identifier', 0)) return { - 'pos.order': self._load_pos_data_read(draft_orders, self.config_id) + 'pos.order': self._load_pos_data_read(draft_orders, self.config_id), } def action_pos_order_cancel(self): @@ -1259,7 +864,7 @@ def sync_from_ui(self, orders): refunded_orders = self._get_refunded_orders(order) if len(refunded_orders) > 1: raise ValidationError(_('You can only refund products from the same order.')) - elif len(refunded_orders) == 1: + if len(refunded_orders) == 1: order_ids.append(refunded_orders[0].id) existing_order = self._get_open_order(order) @@ -1304,7 +909,7 @@ def read_pos_data_uuid(self, uuid): def read_pos_data(self, data, config): # If the previous session is closed, the order will get a new session_id due to _get_valid_session in _process_order - account_moves = self.sudo().account_move | self.sudo().payment_ids.account_move_id | self.sudo().session_move_id + account_moves = self.sudo().account_move | self.sudo().payment_ids.account_move_id | self.session_id.sales_move_id | self.session_id.refunds_move_id return { 'pos.order': self._load_pos_data_read(self, config) if config else [], 'pos.session': [], @@ -1343,7 +948,7 @@ def _prepare_refund_values(self, current_session): def _prepare_mail_values(self, email, ticket, basic_ticket): message = Markup( _("

Dear %(client_name)s,
Here is your Receipt %(is_invoiced)sfor \ - %(pos_name)s amounting in %(amount)s from %(company_name)s.

") + %(pos_name)s amounting in %(amount)s from %(company_name)s.

"), ) % { 'client_name': self.partner_id.name or _('Customer'), 'pos_name': self.name, @@ -1374,7 +979,7 @@ def _refund(self): if not current_session: raise UserError(_('To return product(s), you need to open a session in the POS %s', order.session_id.config_id.display_name)) refund_order = order.copy( - order._prepare_refund_values(current_session) + order._prepare_refund_values(current_session), ) for line in order.lines: if line.refunded_qty < line.qty: @@ -1410,7 +1015,7 @@ def action_send_mail(self): 'default_res_ids': self.ids, 'default_template_id': template.id, }, - 'target': 'new' + 'target': 'new', } def action_send_receipt(self, email): @@ -1429,7 +1034,7 @@ def action_send_receipt(self, email): force_send=True, email_values={ 'email_to': email, - 'attachment_ids': self._get_mail_attachments(self.name, ticket_image, basic_image) + 'attachment_ids': self._get_mail_attachments(self.name, ticket_image, basic_image), }) def _get_mail_attachments(self, name, ticket, basic_ticket): @@ -1465,13 +1070,12 @@ def _get_mail_attachments(self, name, ticket, basic_ticket): 'raw': report[0], 'res_model': 'pos.order', 'res_id': self.ids[0], - 'mimetype': 'application/pdf' + 'mimetype': 'application/pdf', }) attachments += [(4, invoice.id)] return attachments - @api.model def remove_from_ui(self, server_ids): """ Remove orders from the frontend PoS application @@ -1535,22 +1139,567 @@ def _should_update_quantity_on_product(self): """ return True + ############################################################## + # Accounting related methods # + ############################################################## + def _grouping_function(self, base_line): + use_product = self.config_id.use_closing_entry_by_product + product_id = base_line['product_id'] + return frozendict({ + 'account_id': base_line['account_id'], + 'product_id': product_id if use_product else False, + 'tax_ids': base_line['tax_ids'], + }) -class AccountCashRounding(models.Model): - _name = 'account.cash.rounding' - _inherit = ['account.cash.rounding', 'pos.load.mixin'] + def _prepare_move_line_vals_from_base_line(self, base_line): + product = base_line['product_id'] + name = product.display_name if product else _("PoS Miscellaneous") + product_id = product.id if product else False - @api.constrains('rounding', 'rounding_method', 'strategy') - def _check_session_state(self): - open_session = self.env['pos.session'].search([('config_id.rounding_method', 'in', self.ids), ('state', '!=', 'closed')], limit=1) - if open_session: - raise ValidationError( - _("You are not allowed to change the rounding configuration while a pos session using it is already opened.")) + if self.config_id._is_quantities_set(): + quantity = base_line['_aggregated_quantity'] + price_unit = base_line['price_unit'] / quantity if quantity else base_line['price_unit'] + else: + quantity, price_unit = 1.0, base_line['price_unit'] + + line_vals = { + 'name': name, + 'quantity': quantity, + 'price_unit': price_unit, + 'display_type': 'product', + 'extra_tax_data': self.env['account.tax']._export_base_line_extra_tax_data(base_line), + # **base_line['_grouping_key'], + 'account_id': base_line['account_id'].id, + 'tax_ids': [(6, 0, base_line['tax_ids'].ids)], + } + if self.config_id.use_closing_entry_by_product: + line_vals['product_id'] = product_id - @api.model - def _load_pos_data_domain(self, data, config): - return [('id', '=', config.rounding_method.id)] + return line_vals + + def _aggregate_base_line_and_prepare_account_move_line_data(self, base_lines): + def aggregate_function(target_base_line, base_line): + target_base_line.setdefault('_aggregated_quantity', 0.0) + target_base_line['_aggregated_quantity'] += base_line['quantity'] + + AccountTax = self.env['account.tax'] + company = self.company_id + aggregated = AccountTax._reduce_base_lines_with_grouping_function( + base_lines, + grouping_function=self._grouping_function, + aggregate_function=aggregate_function, + ) + AccountTax._fix_base_lines_tax_details_on_manual_tax_amounts( + aggregated, + company, + ) + to_create = [] + for base_line in aggregated: + to_create.append({ + 'account.move.line': self._prepare_move_line_vals_from_base_line(base_line), + 'metadata': { + 'base_line': base_line, + }, + }) + return to_create + + def _prepare_account_move_line_data_from_base_line(self, base_lines): + """ + Convert base_lines to a format compatible with account.move.line Command.create(). + + This produces the same structure as _aggregate_base_line_and_prepare_account_move_line_data + but without any aggregation, each base_line becomes its own entry. + """ + self.ensure_one() # Not aggregated version can only works with on order + if not len(base_lines): + return [] + + to_create = [] + is_percentage = self.pricelist_id and any( + self.pricelist_id.item_ids.filtered( + lambda rule: rule.compute_price == "percentage", + ), + ) + + def add_customer_note(note): + to_create.append({ + 'account.move.line': { + 'name': note, + 'display_type': 'line_note', + }, + 'metadata': {}, + }) + + for base_line in base_lines: + line = base_line['record'] + product = base_line['product_id'] + + if line.product_id.type == 'combo': + to_create.append({ + 'account.move.line': { + 'display_type': 'line_section', + 'name': f"{product.name} x {line.qty}", + 'quantity': abs(line.qty), + 'product_uom_id': line.product_id.uom_id.id, + }, + 'metadata': { + 'line': line, + }, + }) + else: + to_create.append({ + 'account.move.line': { + 'name': product.display_name, + 'display_type': 'product', + 'quantity': base_line['quantity'], + 'discount': base_line['discount'], + 'price_unit': base_line['price_unit'], + 'account_id': base_line['account_id'].id, + 'tax_ids': [(6, 0, base_line['tax_ids'].ids)], + 'extra_tax_data': self.env['account.tax']._export_base_line_extra_tax_data(base_line), + 'product_id': product.id, + 'product_uom_id': base_line['uom_id'].id, + }, + 'metadata': { + 'line': line, + }, + }) + + price_changed = float_compare( + line.price_unit, + line.product_id.lst_price, + precision_rounding=self.currency_id.rounding, + ) + + if is_percentage and price_changed < 0: + decimal = self.currency_id.decimal_places + name = _( + 'Price discount from %(list_price)s to %(price_unit)s', + list_price=float_repr(line.product_id.lst_price, decimal), + price_unit=float_repr(line.price_unit, decimal), + ) + add_customer_note(name) + + if line.customer_note: + add_customer_note(line.customer_note) + + if self.general_customer_note: + add_customer_note(self.general_customer_note) + + return to_create + + def _prepare_account_move_line_data(self, aggregate=True): + """ + Build Command.create entries for account move line, this method + is used in both the invoice generation and the session closing + to create move lines for sales and refunds. + - Session closing: lines are aggregated by taxes + - Order invoice: no aggregation, each line is a separate entry + """ + AccountTax = self.env['account.tax'] + company = self.company_id + base_lines = [] + + # 1. Collect base_lines with PER-ORDER rounding + accounting data + for order in self: + lines = order.lines._prepare_base_lines_for_taxes_computation() + AccountTax._add_tax_details_in_base_lines(lines, company) + AccountTax._round_base_lines_tax_details(lines, company) + AccountTax._add_accounting_data_in_base_lines_tax_details( + lines, + company, + include_caba_tags=True, + ) + base_lines.extend(lines) + + if not aggregate: + # Transform each base_line to the same format as aggregated version, + # but without grouping/aggregation. Each line becomes a separate entry. + AccountTax._fix_base_lines_tax_details_on_manual_tax_amounts( + base_lines, + company, + ) + return self._prepare_account_move_line_data_from_base_line(base_lines) + + # Create zero price line independently, as they will not be aggregated + # This can happen with a fixed taxes, the product is free but the + # tax will add price on the receipt + zero_price_line_data = [line for line in base_lines if line['currency_id'].is_zero(line['price_unit'])] + sales_by_order = defaultdict(list) + zero_price_lines = [] + + for line in zero_price_line_data: + sales_by_order[line['record'].order_id].append(line) + + for [order, line] in sales_by_order.items(): + data = order._prepare_account_move_line_data_from_base_line(line) + zero_price_lines.extend(data) + + # 2. Aggregate product lines by (revenue account + VAT rate) grouping + aggregated_per_line = self._aggregate_base_line_and_prepare_account_move_line_data(base_lines) + return aggregated_per_line + zero_price_lines + + def _prepare_account_move_line_data_for_payments(self, partner=None): + """ + Aggregate pos.payment amounts by payment method for the session receipt. + + This method only COLLECTS and AGGREGATES data, it does NOT create any + accounting records. The actual account.payment records are created + later, after the out_receipt is posted + + Skipped: + - payments whose order is already invoiced (handled by a separate flow) + """ + combined = {} # pm -> total signed amount + session = self.session_id # Always single record in this context + today = fields.Date.context_today(self) + + for payment in self.payment_ids: + pm = payment.payment_method_id + combined.setdefault(pm, 0.0) + combined[pm] += payment.amount + + # Combined payments: aggregate all orders for a given PM into one slot + result = [] + for pm, amount in combined.items(): + partner_account = partner.property_account_receivable_id if partner else None + pm_account = pm.receivable_account_id or session._get_receivable_account() + destination_account = partner_account or pm_account + + result.append({ + 'account.move.line': { + 'display_type': 'payment_term', + 'name': pm.name, + 'account_id': destination_account.id, + 'partner_id': partner.id if partner else None, + 'date_maturity': today, + 'amount_currency': amount, + }, + 'metadata': { + 'payment_method_id': pm, + }, + }) + + return result + + def _prepare_account_move_line_data_for_rounding(self, invoice): + line_ids_commands = [] + currency = self.currency_id + lines = invoice.line_ids.filtered_domain([ + ('display_type', '!=', 'rounding'), + ]) + + debit = sum(lines.mapped('debit')) + credit = sum(lines.mapped('credit')) + invoice_difference = credit - debit + + if invoice.move_type == 'out_refund': + invoice_difference = -invoice_difference + + if not self.config_id.cash_rounding or currency.is_zero(invoice_difference): + return line_ids_commands + + rate = invoice.invoice_currency_rate + difference_balance = invoice.company_currency_id.round(invoice_difference / rate) if rate else 0.0 + signed_difference = (invoice_difference * invoice.direction_sign) + signed_balance = (difference_balance * invoice.direction_sign) + + profit = invoice.invoice_cash_rounding_id.loss_account_id + lost = invoice.invoice_cash_rounding_id.profit_account_id + account = profit if invoice_difference > 0.0 else lost + rounding_line = invoice.line_ids.filtered( + lambda line: line.display_type == 'rounding' and not line.tax_line_id, + ) + + if rounding_line: + line_ids_commands.append(Command.unlink(rounding_line.id)) + + line_ids_commands.append(Command.create({ + 'name': invoice.invoice_cash_rounding_id.name, + 'amount_currency': -signed_difference, + 'balance': -signed_balance, + 'currency_id': invoice.currency_id.id, + 'display_type': 'rounding', + 'account_id': account.id, + })) + + return line_ids_commands @api.model - def _load_pos_data_fields(self, config): - return ['id', 'name', 'rounding', 'rounding_method', 'strategy'] + def _create_payment_moves(self, session, payments, partner=None): + """ + Create one record per PM slot AFTER posting: + - cash PMs => account.bank.statement.line (counterpart on POS receivable) + - other PMs => account.payment + Each produces a credit line on the PM receivable that reconciles against + the receipt's debit payment_term line. + + !!! Only give the partner arguments if you want to use its receivable + account instead of the session's default one (e.g. for invoiced orders) + """ + # Customer account is handled elsewhere... + all_payment_lines = self.env['account.move.line'] + for pm_data in payments: + pm = pm_data['metadata']['payment_method_id'] + amount = pm_data['account.move.line']['amount_currency'] + + arguments = { + 'session': session, + 'amount': amount, + 'partner': partner, + } + + if pm.type == 'cash': + all_payment_lines |= pm._create_cash_payment_line(**arguments) + elif pm.type == 'bank': + all_payment_lines |= pm._create_bank_payment_line(**arguments) + + return all_payment_lines + + def _prepare_invoice_vals(self): + """We have orders filtered by company > config > partners > + fiscal_positions so it won't make any issue when we access user, + partner, bank or similar directly. + """ + config = self.config_id + invoice_date = fields.Date.today() + pos_refunded_invoice_ids = [] + is_single_order = len(self) == 1 + + for orderline in self.lines: + refunded_line = orderline.refunded_orderline_id + if refunded_line.order_id.account_move: + pos_refunded_invoice_ids.append( + refunded_line.order_id.account_move.id, + ) + + is_refund = any(order.is_refund for order in self) + is_total_negative = sum(order.amount_total for order in self) < 0 + move_type = 'out_refund' if is_refund or is_total_negative else 'out_invoice' + partner_term = self.partner_id.property_payment_term_id + is_pay_later = any(p.payment_method_id.type == 'pay_later' for p in self.payment_ids) + invoice_payment_term_id = partner_term.id if partner_term and is_pay_later else False + lines = [] + for order in self: + lines += order._prepare_account_move_line_data(False) + line_commands = [Command.create(line['account.move.line']) for line in lines] + users = self.user_id.ids + fp = self.fiscal_position_id.ids + currencies = self.currency_id.ids + ref = None + + if is_single_order: + ref = _('Customer invoice from %(pos_reference)s', pos_reference=self.name) + + if len(currencies) > 1: + raise UserError(_("You cannot create an invoice for orders with different currencies.")) + + vals = { + 'invoice_origin': ', '.join(ref or '' for ref in self.mapped('pos_reference')), + 'pos_refunded_invoice_ids': pos_refunded_invoice_ids, + 'ref': ref, + 'journal_id': self.config_id.journal_id.id, + 'move_type': move_type, + 'partner_id': self.partner_id.address_get(['invoice'])['invoice'], + 'partner_shipping_id': self.partner_id.address_get(['delivery'])['delivery'], + 'partner_bank_id': self._get_partner_bank_id(), + 'currency_id': self.currency_id.id, + 'invoice_date': invoice_date, + 'invoice_user_id': users[0] if len(users) == 1 else None, + 'fiscal_position_id': fp[0] if fp else None, + 'invoice_line_ids': line_commands, + 'invoice_payment_term_id': invoice_payment_term_id, + } + + refunded_order_invoice = self.refunded_order_id.account_move + if is_single_order and refunded_order_invoice: + vals['ref'] = _('Reversal of: %s', refunded_order_invoice.name) + vals['reversed_entry_id'] = refunded_order_invoice.id + + check_rounding = config.cash_rounding and not config.only_round_cash_method + is_cash_payments = any(p.payment_method_id.type == 'cash' for p in self.payment_ids) + if check_rounding or (config.cash_rounding and is_cash_payments): + vals['invoice_cash_rounding_id'] = config.rounding_method.id + + if any(order.floating_order_name for order in self): + narr = ', '.join(self.filtered('floating_order_name').mapped('floating_order_name')) + vals.update({'narration': narr}) + + return vals + + def _prepare_invoice_extra_line_commands(self, payments=[]): + """ Inherited in pos_stock """ + return [] + + def _generate_pos_order_invoice(self): + if not self.env['res.company']._with_locked_records(self, allow_raising=False): + raise UserError(_("Some orders are dalready being invoiced. Please try again later.")) + + company = self.company_id + vals = self._prepare_invoice_vals() + + lines = [] + total_payments_by_session = {} + for order in self: + payments = order._prepare_account_move_line_data_for_payments( + order.partner_id, + ) + + line_data = [pm['account.move.line'] for pm in payments] + payment_commands = [Command.create(pm_data) for pm_data in line_data] + extra_commands = order._prepare_invoice_extra_line_commands(payments) + lines += payment_commands + extra_commands + total_payments_by_session.setdefault(order.session_id, []) + total_payments_by_session[order.session_id] += payments + + vals['line_ids'] = lines + AccountMove = self.env['account.move'].sudo().with_company(company) + move_ctx = AccountMove.with_context( + default_move_type=vals['move_type'], + check_move_validity=False, + always_tax_exigible=True, + linked_to_pos=True, + ) + invoice = move_ctx.create(vals) + invoice_ctx = invoice.sudo().with_company(company).with_context( + **self._get_invoice_post_context(), + ) + + # Create rounding line if needed + data = self._prepare_account_move_line_data_for_rounding(invoice) + with self.env['account.move']._check_balanced({'records': invoice}): + invoice.with_context(linked_to_pos=True).line_ids = data + + # Set account_move before _post() so that invoice.pos_order_ids is + # populated when stock_account computes COGS via _get_cogs_value() + # (pos_stock overrides _get_cogs_value to use _get_pos_anglo_saxon_price_unit + # which relies on pos_order_ids being set on the move). + self.account_move = invoice + invoice_ctx._post() + payment_term_lines = invoice.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + + if not self.env.context.get('skip_payment'): + all_payment_lines = self.env['account.move.line'] + for session, payments in total_payments_by_session.items(): + all_payment_lines |= self._create_payment_moves( + session, + payments, + self.partner_id, + ) + + to_reconcile = (payment_term_lines | all_payment_lines).filtered( + lambda line: not line.reconciled, + ) + to_reconcile.with_context(skip_invoice_sync=True).reconcile() + + body = _("This invoice has been created from the point of sale session:%s", + Markup().join(Markup("%s ") % order._get_html_link() for order in self), + ) + invoice.message_post(body=body) + if self.env.context.get('generate_pdf', True): + invoice.with_context(skip_invoice_sync=True)._generate_and_send() + else: + order.defer_invoice_pdf = True + + return invoice + + def _prepare_missing_invoice_moves(self): + self.write({'to_invoice': True}) + return self._generate_pos_order_invoice() + + def action_invoice_download_pdf(self): + self.ensure_one() + if self.defer_invoice_pdf: + self.account_move.with_context(skip_invoice_sync=True)._generate_and_send() + self.defer_invoice_pdf = False + return self.account_move.action_invoice_download_pdf() + + def action_pos_order_invoice(self): + self.ensure_one() + + if self.invoice_status == 'invoiced': + # Already has a real customer invoice (account_move is not a session closing entry). + move = self.account_move + elif self.session_id and self.session_id.state == 'closed': + # Session is closed: reverse the closing entry and create a proper invoice. + move = self._generate_invoice_after_session_closing() + else: + # Session still open: standard path (also handles pos_stock picking creation). + move = self._prepare_missing_invoice_moves() + + return { + 'name': _('Customer Invoice'), + 'view_mode': 'form', + 'view_id': self.env.ref('account.view_move_form').id, + 'res_model': 'account.move', + 'context': "{'move_type':'out_invoice'}", + 'type': 'ir.actions.act_window', + 'target': 'current', + 'res_id': move.id, + } + + def _generate_invoice_after_session_closing(self): + """ + This method will reverse the corresponding amount from the global + session closing entry and create a new invoice with the same + amount, so that the invoice will be correctly taken into account + in the fiscal report of the day of the order, and not in the day + of the session closing. + """ + self.ensure_one() + if not self.session_id or self.session_id.state != "closed": + return self._generate_pos_order_invoice() + + session = self.session_id + refund_move = session.refunds_move_id + sale_move = session.sales_move_id + global_move = refund_move if self.is_refund else sale_move + if not global_move or global_move.state != "posted": + return self.env['account.move'] + + invoice = self.with_context( + skip_payment=True, + )._generate_pos_order_invoice() + session = self.session_id + move = session._create_partial_reversal_move_from_session_closing(self) + move._post() + + # Create misc move to balance the invoice with the correct amount, + # with a counterpart on the same account as the session closing + # entry, so that the fiscal report is correct. + sign = -1 if self.is_refund else 1 + counter_strike = self.env['account.move'].sudo().with_context( + default_move_type='entry', + ).create({ + 'ref': _("Account transfer for invoice %s", invoice.name), + 'date': invoice.invoice_date, + 'journal_id': global_move.journal_id.id, + 'line_ids': [ + Command.create({ + 'name': _("Counterpart for invoice %s", invoice.name), + 'account_id': invoice.partner_id.property_account_receivable_id.id, + 'balance': -invoice.amount_total * sign, + }), + Command.create({ + 'name': _("Reversal for invoice %s payment", invoice.name), + 'account_id': global_move.line_ids[0].account_id.id, + 'balance': invoice.amount_total * sign, + }), + ], + }) + + counter_strike.action_post() + invoice_payment_line = invoice.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + line_for_invoice = counter_strike.line_ids.filtered( + lambda line: line.account_id == invoice_payment_line.account_id, + ) + + if invoice_payment_line and line_for_invoice: + to_reconcile = invoice_payment_line | line_for_invoice + to_reconcile.with_context(skip_invoice_sync=True).reconcile() + + return invoice diff --git a/addons/point_of_sale/models/pos_order_line.py b/addons/point_of_sale/models/pos_order_line.py index 7ab5ca53656227..11d0b19cff5270 100644 --- a/addons/point_of_sale/models/pos_order_line.py +++ b/addons/point_of_sale/models/pos_order_line.py @@ -1,11 +1,13 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from uuid import uuid4 + from markupsafe import Markup -from odoo import api, Command, fields, models, _ -from odoo.tools import float_is_zero +from odoo import _, api, fields, models from odoo.exceptions import UserError +from odoo.fields import Command +from odoo.tools import float_is_zero class PosOrderLine(models.Model): @@ -81,7 +83,7 @@ def _load_pos_data_fields(self, config): @api.depends('refund_orderline_ids', 'refund_orderline_ids.order_id.state') def _compute_refund_qty(self): for orderline in self: - refund_order_line = orderline.refund_orderline_ids.filtered(lambda l: l.order_id.state != 'cancel') + refund_order_line = orderline.refund_orderline_ids.filtered(lambda line: line.order_id.state != 'cancel') orderline.refunded_qty = -sum(refund_order_line.mapped('qty')) def _prepare_refund_data(self, refund_order): @@ -130,7 +132,7 @@ def write(self, vals): def _unlink_except_order_state(self): if self.filtered(lambda x: x.order_id.state not in ["draft", "cancel"]): raise UserError(_("You can only unlink PoS order lines that are related to orders in new or cancelled state.")) - for line in self.filtered(lambda l: l.order_id.config_id.order_edit_tracking): + for line in self.filtered(lambda line: line.order_id.config_id.order_edit_tracking): line.order_id.has_deleted_line = True body = _("%(product_name)s: Deleted line (quantity: %(qty)s)", product_name=line.full_product_name, qty=line.qty) line.order_id.message_post(body=line.order_id._prepare_pos_log(body)) @@ -143,22 +145,28 @@ def _onchange_amount_line_all(self): def _compute_amount_line_all(self, qty=None): self.ensure_one() - sign = -1 if self.order_id.is_refund else 1 fpos = self.order_id.fiscal_position_id tax_ids_after_fiscal_position = fpos.map_tax(self.tax_ids) price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) - line_qty = qty or (self.qty * sign) - taxes = tax_ids_after_fiscal_position.compute_all(price, self.order_id.currency_id, line_qty, product=self.product_id, partner=self.order_id.partner_id) + line_qty = qty or self.qty + line_qty = -abs(line_qty) if self.order_id.is_refund else line_qty # All lines quantity must be negative in refund + taxes = tax_ids_after_fiscal_position.compute_all( + price, + self.order_id.currency_id, + line_qty, + product=self.product_id, + partner=self.order_id.partner_id, + ) return { - 'price_subtotal_incl': taxes['total_included'], - 'price_subtotal': taxes['total_excluded'], + 'price_subtotal_incl': abs(taxes['total_included']), # Line prices are always positive + 'price_subtotal': abs(taxes['total_excluded']), # Line prices are always positive } @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: price = self.order_id.pricelist_id._get_product_price( - self.product_id, self.qty or 1.0, currency=self.currency_id + self.product_id, self.qty or 1.0, currency=self.currency_id, ) self.tax_ids = self.product_id.taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(self.company_id)) tax_ids_after_fiscal_position = self.order_id.fiscal_position_id.map_tax(self.tax_ids) @@ -190,9 +198,7 @@ def _compute_total_cost(self, at_closing=False): """ Compute the total cost of the order lines. """ - for line in self: - if line.is_total_cost_computed: - continue + for line in self.filtered(lambda line: not line.is_total_cost_computed): product = line.product_id cost_currency = product.sudo().cost_currency_id product_cost = line._get_product_cost(at_closing) @@ -214,58 +220,10 @@ def _compute_margin(self): line.margin_percent = 0 else: line.margin = (line.price_subtotal * sign) - line.total_cost - line.margin_percent = not float_is_zero(line.price_subtotal, precision_rounding=line.currency_id.rounding) \ - and line.margin / (line.price_subtotal * sign) \ + line.margin_percent = (not float_is_zero(line.price_subtotal, precision_rounding=line.currency_id.rounding) + and line.margin / (line.price_subtotal * sign)) \ or 0 - def _prepare_base_line_for_taxes_computation(self): - self.ensure_one() - commercial_partner = self.order_id.partner_id.commercial_partner_id - fiscal_position = self.order_id.fiscal_position_id - line = self.with_company(self.order_id.company_id) - account = line.product_id._get_product_accounts()['income'] or self.order_id.config_id.journal_id.default_account_id - if not account: - raise UserError(_( - "Please define income account for this product: '%(product)s' (id:%(id)d).", - product=line.product_id.name, id=line.product_id.id, - )) - - if fiscal_position: - account = fiscal_position.map_account(account) - - is_refund_order = line.order_id.is_refund or line.order_id.amount_total < 0.0 - is_refund_line = line.qty * line.price_unit < 0 - - lang = line.order_id.partner_id.lang or self.env.user.lang - product_name = line.with_context(lang=lang).full_product_name or line.product_id.with_context(lang=lang).display_name - if line.product_id.description_sale: - product_name += '\n' + line.product_id.with_context(lang=lang).description_sale - return { - **self.env['account.tax']._prepare_base_line_for_taxes_computation( - line, - partner_id=commercial_partner, - currency_id=self.order_id.currency_id, - rate=self.order_id.currency_rate, - product_id=line.product_id, - tax_ids=line.tax_ids_after_fiscal_position, - price_unit=line.price_unit, - quantity=line.qty * (-1 if is_refund_order else 1), - discount=line.discount, - account_id=account, - is_refund=is_refund_line, - sign=1 if is_refund_order else -1, - ), - 'uom_id': line.product_uom_id, - 'name': product_name, - } - - def _prepare_tax_base_line_values(self): - """ - Convert pos order lines into dictionaries that would be used to compute taxes later. - :return: A list of python dictionaries (see '_prepare_base_line_for_taxes_computation' in account.tax). - """ - return [line._prepare_base_line_for_taxes_computation() for line in self] - def _get_discount_amount(self): self.ensure_one() original_price = self.tax_ids_after_fiscal_position.compute_all(self.price_unit, self.currency_id, self.qty, product=self.product_id, partner=self.order_id.partner_id)['total_included'] @@ -274,3 +232,48 @@ def _get_discount_amount(self): def _get_product_cost(self, at_closing=False): self.ensure_one() return self.product_id.standard_price + + ############################################################## + # Accounting related methods # + ############################################################## + def _prepare_base_lines_for_taxes_computation(self): + base_lines = [] + is_refund = self.order_id.is_refund + commercial_partner = self.order_id.partner_id.commercial_partner_id + fiscal_position = self.order_id.fiscal_position_id + + for record in self: + line = record.with_company(record.order_id.company_id) + lang = line.order_id.partner_id.lang or record.env.user.lang + account = line.product_id._get_product_accounts()['income'] or record.order_id.config_id.journal_id.default_account_id + product_name = line.with_context(lang=lang).full_product_name or line.product_id.with_context(lang=lang).display_name + + if not account: + raise UserError(_( + "Please define income account for this product: '%(product)s' (id:%(id)d).", + product=line.product_id.name, id=line.product_id.id, + )) + + if fiscal_position: + account = fiscal_position.map_account(account) + + base_lines.append({ + **record.env['account.tax']._prepare_base_line_for_taxes_computation( + line, + partner_id=commercial_partner, + currency_id=record.order_id.currency_id, + rate=record.order_id.currency_rate, + product_id=line.product_id, + tax_ids=line.tax_ids_after_fiscal_position, + price_unit=line.price_unit, + quantity=line.qty * (-1 if is_refund else 1), + discount=line.discount, + account_id=account, + is_refund=is_refund, + sign=-1 if is_refund else 1, + ), + 'uom_id': line.product_uom_id, + 'name': product_name, + }) + + return base_lines diff --git a/addons/point_of_sale/models/pos_payment.py b/addons/point_of_sale/models/pos_payment.py index 62f837e710ba1e..8be7043f8a7088 100644 --- a/addons/point_of_sale/models/pos_payment.py +++ b/addons/point_of_sale/models/pos_payment.py @@ -1,8 +1,9 @@ -from odoo import api, fields, models, _ -from odoo.tools import formatLang, float_is_zero -from odoo.exceptions import ValidationError from uuid import uuid4 +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools import float_compare, formatLang + class PosPayment(models.Model): """ Used to register payments made in a pos.order. @@ -79,88 +80,42 @@ def _check_payment_method_id(self): if payment.payment_method_id not in payment.session_id.config_id.payment_method_ids: raise ValidationError(_('The payment method selected is not allowed in the config of the POS session.')) - def _create_payment_moves(self, is_reverse=False): - result = self.env['account.move'] - change_payment = self.filtered(lambda p: p.is_change and p.payment_method_id.type == 'cash') - payment_to_change = self.filtered(lambda p: not p.is_change and p.payment_method_id.type == 'cash')[:1] - payments = self - if change_payment and payment_to_change: - payments = self - change_payment - - for payment in payments: - order = payment.pos_order_id - payment_method = payment.payment_method_id - if payment_method.type == 'pay_later' or float_is_zero(payment.amount, precision_rounding=order.currency_id.rounding): - continue - accounting_partner = self.env["res.partner"]._find_accounting_partner(payment.partner_id) - pos_session = order.session_id - journal = pos_session.config_id.journal_id - if change_payment and payment == payment_to_change: - pos_payment_ids = payment.ids + change_payment.ids - payment_amount = payment.amount + change_payment.amount - else: - pos_payment_ids = payment.ids - payment_amount = payment.amount - payment_move = self.env['account.move'].with_context(default_journal_id=journal.id).create({ - 'journal_id': journal.id, - 'date': fields.Date.context_today(order, order.date_order), - 'ref': _('Invoice payment for %(order)s (%(account_move)s) using %(payment_method)s', order=order.name, account_move=order.account_move.name, payment_method=payment_method.name), - 'pos_payment_ids': pos_payment_ids, - }) - result |= payment_move - payment.write({'account_move_id': payment_move.id}) - amounts = pos_session._update_amounts({'amount': 0, 'amount_converted': 0}, {'amount': payment_amount}, payment.payment_date) - credit_line_vals = pos_session._credit_amounts({ - 'account_id': accounting_partner.with_company(order.company_id).property_account_receivable_id.id, # The field being company dependant, we need to make sure the right value is received. - 'partner_id': accounting_partner.id, - 'move_id': payment_move.id, - 'no_followup': False, - }, amounts['amount'], amounts['amount_converted']) - is_split_transaction = payment.payment_method_id.split_transactions - if is_split_transaction and is_reverse: - reversed_move_receivable_account_id = accounting_partner.with_company(order.company_id).property_account_receivable_id.id - elif is_reverse: - reversed_move_receivable_account_id = payment.payment_method_id.receivable_account_id.id or self.company_id.account_default_pos_receivable_account_id.id - else: - reversed_move_receivable_account_id = self.company_id.account_default_pos_receivable_account_id.id - debit_line_vals = pos_session._debit_amounts({ - 'account_id': reversed_move_receivable_account_id, - 'move_id': payment_move.id, - 'partner_id': accounting_partner.id if is_split_transaction and is_reverse else False, - 'no_followup': False, - }, amounts['amount'], amounts['amount_converted']) - self.env['account.move.line'].create([credit_line_vals, debit_line_vals]) - payment_move._post() - return result - - def _get_receivable_lines_for_invoice_reconciliation(self, receivable_account): - """ - If this payment is linked to an account.move, this returns the corresponding receivable lines - that should be reconciled with the invoice's receivable lines. - The introduced heuristics here is important for cases where the pos receivable account is the same - as the receivable account of the customer. - - - positive payment -> negative balance lines - - negative payment -> positive balance lines - """ - - result = self.env['account.move.line'] - for payment in self: - if not payment.account_move_id: - continue - - currency = payment.currency_id - is_positive_amount = currency.compare_amounts(payment.amount, 0) > 0 - - for line in payment.account_move_id.line_ids: - if currency.compare_amounts(line.balance, 0) == 0 or line.account_id != receivable_account or line.reconciled: - continue - - if is_positive_amount: - if currency.compare_amounts(line.balance, 0) < 0: - result |= line - else: - if currency.compare_amounts(line.balance, 0) > 0: - result |= line - - return result + ############################################################## + # Accounting related methods # + ############################################################## + def _create_split_account_payment(self, amount): + self.ensure_one() + payment_method = self.payment_method_id + if not payment_method.journal_id: + return self.env['account.move.line'] + + rounding = self.currency_id.rounding + outstanding_account = payment_method.outstanding_account_id + accounting_partner = self.env["res.partner"]._find_accounting_partner(self.partner_id) + destination_account = accounting_partner.property_account_receivable_id + + # revert the accounts because account.payment doesn't accept negative amount. + if float_compare(amount, 0, precision_rounding=rounding) < 0: + outstanding_account, destination_account = destination_account, outstanding_account + + memo = _( + '%(payment_method)s POS payment of %(partner)s in %(session)s', + payment_method=payment_method.name, + partner=self.partner_id.display_name, + session=self.name, + ) + account_payment = self.env['account.payment'].create({ + 'amount': amount, + 'partner_id': accounting_partner.id, + 'journal_id': payment_method.journal_id.id, + 'force_outstanding_account_id': outstanding_account.id, + 'destination_account_id': destination_account.id, + 'memo': memo, + 'pos_payment_method_id': payment_method.id, + 'pos_session_id': self.id, + }) + + account_payment.action_post() + return account_payment.move_id.line_ids.filtered( + lambda line: line.account_id == destination_account, + ) diff --git a/addons/point_of_sale/models/pos_payment_method.py b/addons/point_of_sale/models/pos_payment_method.py index fe19860acddc88..8104a25618fdf7 100644 --- a/addons/point_of_sale/models/pos_payment_method.py +++ b/addons/point_of_sale/models/pos_payment_method.py @@ -1,6 +1,6 @@ from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError -from odoo.tools import BinaryBytes, file_open +from odoo.tools import BinaryBytes, file_open, float_compare class PosPaymentMethod(models.Model): @@ -36,9 +36,6 @@ def _get_payment_method_type(self): selection.append(('bank_qr_code', self.env._('Bank App (QR Code)'))) return selection - def _is_online_payment(self): - return False - name = fields.Char(string="Method", required=True, translate=True, help='Defines the name of the payment method that will be displayed in the Point of Sale when the payments are selected.') sequence = fields.Integer(copy=False, default=_default_sequence) outstanding_account_id = fields.Many2one('account.account', @@ -53,7 +50,6 @@ def _is_online_payment(self): check_company=True, help="Leave empty to use the default account from the company setting.\n" "Overrides the company's receivable account (for Point of Sale) used in the journal entries.") - is_cash_count = fields.Boolean(string='Cash', compute="_compute_is_cash_count", store=True) journal_id = fields.Many2one('account.journal', string='Journal', domain=['|', '&', ('type', '=', 'cash'), ('pos_payment_method_ids', '=', False), ('type', '=', 'bank')], @@ -65,10 +61,10 @@ def _is_online_payment(self): 'For cash journal, we directly write to the default account in the journal via statement lines.\n' 'For bank journal, we write to the outstanding account specified in this payment method.\n' 'Only cash and bank journals are allowed.') - split_transactions = fields.Boolean( - string='Identify Customer', - default=False, - help='Forces to set a customer when using this payment method and splits the journal entries for each customer. It could slow down the closing process.') + account_bank_statement_id = fields.Many2one( + 'account.bank.statement', + string='Cash Lines', + readonly=True) open_session_ids = fields.Many2many('pos.session', string='Pos Sessions', compute='_compute_open_session_ids', help='Open PoS sessions that are using this payment method.') config_ids = fields.Many2many('pos.config', string='Point of Sale', check_company=True) company_id = fields.Many2one( @@ -79,7 +75,13 @@ def _is_online_payment(self): ) default_pos_receivable_account_name = fields.Char(related="company_id.account_default_pos_receivable_account_id.display_name", string="Default Receivable Account Name") active = fields.Boolean(default=True) - type = fields.Selection(selection=[('cash', 'Cash'), ('bank', 'Bank'), ('pay_later', 'Customer Account')], compute="_compute_type") + type = fields.Selection( + selection=[ + ('cash', 'Cash'), + ('bank', 'Bank'), + ('pay_later', 'Customer Account'), + ], + required=True) custom_image = fields.Image("Custom Image", max_width=90, max_height=90) image = fields.Image(max_width=90, max_height=90, compute="_compute_image", inverse="_inverse_image") payment_method_type = fields.Selection(selection=lambda self: self._get_payment_method_type(), string="Integration", default='none', required=True) @@ -92,7 +94,6 @@ def _is_online_payment(self): help='Type of QR-code to be generated for this payment method.', ) hide_qr_code_method = fields.Boolean(compute='_compute_hide_qr_code_method') - payment_provider = fields.Selection(selection=lambda self: self._get_provider_selection(), string='Payment Provider', help='Payment provider that will be used to process payments made with this payment method.') available_payment_providers = fields.Json(compute='_compute_available_payment_providers') @@ -131,7 +132,7 @@ def _load_pos_data_domain(self, data, config): @api.model def _load_pos_data_fields(self, config): - return ['id', 'name', 'is_cash_count', 'payment_provider', 'split_transactions', 'type', 'image', 'sequence', 'payment_method_type', 'default_qr'] + return ['id', 'name', 'payment_provider', 'type', 'image', 'sequence', 'payment_method_type', 'default_qr'] @api.depends('payment_method_type') def _compute_hide_qr_code_method(self): @@ -169,7 +170,16 @@ def _compute_open_session_ids(self): for payment_method in self: payment_method.open_session_ids = self.env['pos.session'].search([('config_id', 'in', payment_method.config_ids.ids), ('state', '!=', 'closed')]) - @api.depends('journal_id', 'split_transactions') + @api.constrains('journal_id', 'type') + def _constraint_journal_payment_method_type(self): + for record in self: + if record.type not in ['cash', 'bank']: + continue + + if record.journal_id and record.type != record.journal_id.type: + raise ValidationError(_("The type of the payment method must be the same as the type of the journal.")) + + @api.depends('journal_id') def _compute_type(self): for pm in self: if pm.journal_id.type in {'cash', 'bank'}: @@ -222,11 +232,6 @@ def _onchange_journal_id(self): chart_template = self.with_context(allowed_company_ids=self.env.company.root_id.ids).env['account.chart.template'] pm.outstanding_account_id = chart_template.ref('account_journal_payment_debit_account_id', raise_if_not_found=False) or self.company_id.transfer_account_id - @api.depends('type') - def _compute_is_cash_count(self): - for pm in self: - pm.is_cash_count = pm.type == 'cash' - def _compute_all_providers_installed(self): providers_status = self.get_provider_status() if providers_status and all(status['state'] == 'installed' for status in providers_status): @@ -260,7 +265,12 @@ def create(self, vals_list): for vals in vals_list: if vals.get('payment_method_type', False): self._force_payment_method_type_values(vals, vals['payment_method_type']) - return super().create(vals_list) + payment_methods = super().create(vals_list) + for pm in payment_methods: + if pm.type != 'cash': + continue + + return payment_methods def write(self, vals): if self._is_write_forbidden(set(vals.keys())): @@ -370,3 +380,89 @@ def get_qr_code_url(self, amount, free_communication, structured_communication, return payment_bank.with_context(is_online_qr=True).build_qr_code_url( float(amount), free_communication, structured_communication, currency, debtor_partner, self.qr_code_method, silent_errors=False) + + ############################################################## + # Accounting related methods # + ############################################################## + def _create_bank_payment_line(self, session, amount, partner=None, message=None): + self.ensure_one() + outstanding_account = self.outstanding_account_id + partner_account = partner.property_account_receivable_id if partner else None + pm_account = self.receivable_account_id + session_account = session._get_receivable_account() + destination_account = partner_account or pm_account or session_account + rounding = session.currency_id.rounding + + # TODO: add a list of pos.order that was paid though this combined PM + session_ref = _( + 'Combine %(payment_method)s POS payments from %(session)s', + payment_method=self.name, + session=session.name, + ) + account_payment = self.env['account.payment'].sudo().create({ + 'amount': abs(amount), + 'journal_id': self.journal_id.id, + 'force_outstanding_account_id': outstanding_account.id, + 'destination_account_id': destination_account.id, + 'memo': message or session_ref, + 'payment_reference': session_ref, + 'pos_payment_method_id': self.id, + 'pos_session_id': session.id, + 'partner_id': partner.id if partner else None, + 'company_id': self.company_id.id, + }) + + if float_compare(amount, 0, precision_rounding=rounding) < 0: + # For refunds, only flip the payment direction don't swap accounts. + # outstanding_account_id is computed from force_outstanding_account_id + # and cannot be overridden by a direct write, so swapping via + # outstanding_account_id silently fails and leaves both sides on the same account. + account_payment.write({'payment_type': 'outbound'}) + + account_payment.action_post() + if float_compare(amount, 0, precision_rounding=rounding) < 0: + # Outbound: receivable sits on the debit side. + return account_payment.move_id.line_ids.filtered( + lambda line: line.account_id == destination_account and line.debit > 0, + ) + + return account_payment.move_id.line_ids.filtered( + lambda line: line.account_id == destination_account, + ) + + def _create_cash_payment_line(self, session, amount, partner=None, message=None): + """ + Use account.bank.statement.line for cash PMs. + Pass counterpart_account_id to bypass the journal suspense account + and land the counterpart directly on the POS receivable, so it + can be reconciled with the out_receipt payment_term line below. + """ + self.ensure_one() + if self.type != 'cash': + raise ValueError(_('Only cash payment methods can use cash payment lines.')) + + BankStatementLine = self.env['account.bank.statement.line'].with_context( + no_retrieve_partner=True, + ) + partner_account = partner.property_account_receivable_id if partner else None + pm_account = self.receivable_account_id + session_account = session._get_receivable_account() + destination_account = partner_account or pm_account or session_account + statement_line = BankStatementLine.sudo().create({ + 'amount': amount, + 'company_id': session.company_id.id, + 'journal_id': self.journal_id.id, + 'date': fields.Date.context_today(self), + 'partner_id': partner.id if partner else None, + 'statement_id': session.bank_statement_id.id, + 'pos_session_id': session.id, + 'counterpart_account_id': destination_account.id, + 'payment_ref': message or _( + '%(payment_method)s POS payments from %(session)s', + payment_method=self.name, + session=session.name, + ), + }) + return statement_line.move_id.line_ids.filtered( + lambda line, acc=destination_account: line.account_id == acc, + ) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 7f3ed8b62ed51f..2b5769ed0b5c4f 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -1,14 +1,12 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -from collections import defaultdict +import logging from datetime import timedelta -from itertools import starmap + from markupsafe import Markup -import logging -from odoo import api, fields, models, _ +from odoo import Command, _, api, fields, models from odoo.exceptions import AccessError, UserError, ValidationError -from odoo.fields import Command, Domain -from odoo.tools import float_is_zero, float_compare, frozendict, plaintext2html +from odoo.tools import float_is_zero, plaintext2html _logger = logging.getLogger(__name__) @@ -20,19 +18,24 @@ class PosSession(models.Model): _inherit = ['mail.thread', 'mail.activity.mixin', "pos.bus.mixin", 'pos.load.mixin'] POS_SESSION_STATE = [ - ('opening_control', 'Opening Control'), # method action_pos_session_open - ('opened', 'In Progress'), # method action_pos_session_closing_control - ('closing_control', 'Closing Control'), # method action_pos_session_close + ('opening_control', 'Opening Control'), + ('opened', 'In Progress'), + ('closing_control', 'Closing Control'), ('closed', 'Closed & Posted'), ] - company_id = fields.Many2one('res.company', related='config_id.company_id', string="Company", readonly=True) - + company_id = fields.Many2one( + 'res.company', + related='config_id.company_id', + string="Company", + readonly=True, + ) config_id = fields.Many2one( - 'pos.config', string='Point of Sale', + 'pos.config', + string='Point of Sale', required=True, - index=True) - name = fields.Char(string='Session ID', readonly=True, default='/') + index=True, + ) user_id = fields.Many2one( 'res.users', string='Opened By', required=True, @@ -40,59 +43,110 @@ class PosSession(models.Model): readonly=False, default=lambda self: self.env.uid, ondelete='restrict') - currency_id = fields.Many2one('res.currency', related='config_id.currency_id', string="Currency", readonly=False) + currency_id = fields.Many2one( + 'res.currency', + related='config_id.currency_id', + string="Currency", + readonly=False, + ) + name = fields.Char(string='Session ID', readonly=True, default='/') start_at = fields.Datetime(string='Opening Date', readonly=True) stop_at = fields.Datetime(string='Closing Date', readonly=True, copy=False) - - state = fields.Selection( - POS_SESSION_STATE, string='Status', - required=True, readonly=True, - index=True, copy=False, default='opening_control') - opening_notes = fields.Text(string="Opening Notes") closing_notes = fields.Text(string="Closing Notes") - cash_control = fields.Boolean(compute='_compute_cash_control', string='Has Cash Control') - cash_journal_id = fields.Many2one('account.journal', compute='_compute_cash_journal', string='Cash Journal', store=True) - - cash_register_balance_end_real = fields.Monetary( - string="Ending Balance", - readonly=True) - cash_register_balance_start = fields.Monetary( - string="Starting Balance", + state = fields.Selection( + POS_SESSION_STATE, + string='Status', + required=True, + readonly=True, + index=True, + copy=False, + default='opening_control', + ) + + # Cash control fields + bank_statement_id = fields.Many2one( + 'account.bank.statement', + string='Bank Statement', + index='btree_not_null', readonly=True) - cash_register_balance_end = fields.Monetary( - compute='_compute_cash_balance', - string="Theoretical Closing Balance", - help="Opening balance summed to all cash transactions.", + bank_statement_line_ids = fields.One2many( + 'account.bank.statement.line', + related='bank_statement_id.line_ids', + string='Bank Statement Lines', readonly=True) - cash_register_difference = fields.Monetary( - compute='_compute_cash_balance', - string='Before Closing Difference', - help="Difference between the theoretical closing balance and the real closing balance.", + opening_balance = fields.Monetary( + string='Opening Balance', + related='bank_statement_id.balance_start', readonly=True) - - # Total Cash In/Out - cash_real_transaction = fields.Monetary(string='Transaction', readonly=True) + closing_balance = fields.Monetary( + string='Closing Balance', + related='bank_statement_id.balance_end_real', + readonly=True, + ) + closing_difference = fields.Monetary( + string='Closing Difference', + compute='_compute_closing_difference', + ) + cash_control = fields.Boolean( + related='config_id.cash_control', + string='Cash Control', + readonly=True, + store=True) # Need to be stored in case of change of config + sales_move_id = fields.Many2one( + 'account.move', + string='Sales Entry', + index=True, + ) + refunds_move_id = fields.Many2one( + 'account.move', + string='Refunds Entry', + index=True, + ) + correction_move_ids = fields.Many2many( + 'account.move', + string='Correction Entries', + index=True, + ) + move_ids = fields.Many2many( + 'account.move', + string='Related Journal Entries', + compute='_compute_move_ids', + search='_search_move_ids', + ) order_ids = fields.One2many('pos.order', 'session_id', string='Orders') order_count = fields.Integer(compute='_compute_order_count') - statement_line_ids = fields.One2many('account.bank.statement.line', 'pos_session_id', string='Cash Lines', readonly=True) - rescue = fields.Boolean(string='Recovery Session', + rescue = fields.Boolean( + string='Recovery Session', help="Auto-generated session for orphan orders, ignored in constraints", readonly=True, copy=False) - move_id = fields.Many2one('account.move', string='Journal Entry', index=True) - payment_method_ids = fields.Many2many('pos.payment.method', related='config_id.payment_method_ids', string='Payment Methods') - total_payments_amount = fields.Float(compute='_compute_total_payments_amount', string='Total Payments Amount') - is_in_company_currency = fields.Boolean('Is Using Company Currency', compute='_compute_is_in_company_currency') - bank_payment_ids = fields.One2many('account.payment', 'pos_session_id', 'Bank Payments', help='Account payments representing aggregated and bank split payments.') + payment_method_ids = fields.Many2many( + 'pos.payment.method', + related='config_id.payment_method_ids', + string='Payment Methods', + ) + total_payments_amount = fields.Float( + compute='_compute_total_payments_amount', + string='Total Payments Amount', + ) + is_in_company_currency = fields.Boolean( + 'Is Using Company Currency', + compute='_compute_is_in_company_currency', + ) + + @api.depends('closing_balance', 'opening_balance') + def _compute_closing_difference(self): + for record in self: + record.closing_difference = record.closing_balance - record.opening_balance def write(self, vals): if vals.get('state') == 'closed': for record in self: record.config_id._notify(('CLOSING_SESSION', { 'device_identifier': self.env.context.get('device_identifier', False), - 'session_id': record.id + 'session_id': record.id, })) return super().write(vals) @@ -132,11 +186,19 @@ def _load_pos_data_relations(self, model, fields): @api.model def _load_pos_data_models(self, config): - return ['pos.config', 'pos.preset', 'resource.calendar.attendance', 'pos.order', 'pos.order.line', 'pos.payment', 'pos.payment.method', 'pos.printer', - 'pos.category', 'pos.bill', 'res.company', 'account.tax', 'account.tax.group', 'product.template', 'product.product', 'product.attribute', 'product.attribute.custom.value', - 'product.template.attribute.line', 'product.template.attribute.value', 'product.combo', 'product.combo.item', 'res.users', 'res.partner', 'product.uom', - 'decimal.precision', 'uom.uom', 'res.country', 'res.country.state', 'res.lang', 'product.category', 'product.pricelist', 'product.pricelist.item', - 'account.cash.rounding', 'account.fiscal.position', 'res.currency', 'pos.note', 'product.tag', 'ir.module.module', 'account.move', 'account.account', 'pos.product.template.snooze'] + return [ + 'pos.config', 'pos.preset', 'resource.calendar.attendance', 'pos.order', + 'pos.order.line', 'pos.payment', 'pos.payment.method', 'pos.printer', + 'pos.category', 'pos.bill', 'res.company', 'account.tax', 'account.tax.group', + 'product.template', 'product.product', 'product.attribute', 'product.attribute.custom.value', + 'product.template.attribute.line', 'product.template.attribute.value', + 'product.combo', 'product.combo.item', 'res.users', 'res.partner', + 'product.uom', 'decimal.precision', 'uom.uom', 'res.country', 'res.country.state', + 'res.lang', 'product.category', 'product.pricelist', 'product.pricelist.item', + 'account.cash.rounding', 'account.fiscal.position', 'res.currency', 'pos.note', + 'product.tag', 'ir.module.module', 'account.move', 'account.account', + 'pos.product.template.snooze', + ] @api.model def _load_pos_data_domain(self, data, config): @@ -146,7 +208,7 @@ def _load_pos_data_domain(self, data, config): def _load_pos_data_fields(self, config): return [ 'id', 'name', 'user_id', 'config_id', 'start_at', 'stop_at', - 'payment_method_ids', 'state', 'cash_register_balance_start', 'access_token' + 'payment_method_ids', 'state', 'access_token', ] def load_data(self, models_to_load): @@ -170,14 +232,14 @@ def load_data_params(self): fields = self._load_pos_data_fields(self.config_id) response['pos.session'] = { 'fields': fields, - 'relations': self._load_pos_data_relations('pos.session', fields) + 'relations': self._load_pos_data_relations('pos.session', fields), } for model in self._load_pos_data_models(self.config_id): fields = self.env[model]._load_pos_data_fields(self.config_id) response[model] = { 'fields': fields, - 'relations': self._load_pos_data_relations(model, fields) + 'relations': self._load_pos_data_relations(model, fields), } return response @@ -229,7 +291,7 @@ def get_pos_ui_product_pricelist_item_by_product(self, product_tmpl_ids, product return { 'product.pricelist.item': pricelist_item.read(pricelist_item_fields, load=False), - 'product.pricelist': pricelist.read(pricelist_fields, load=False) + 'product.pricelist': pricelist.read(pricelist_fields, load=False), } @api.depends('currency_id', 'company_id.currency_id') @@ -237,26 +299,6 @@ def _compute_is_in_company_currency(self): for session in self: session.is_in_company_currency = session.currency_id == session.company_id.currency_id - @api.depends('payment_method_ids', 'order_ids', 'cash_register_balance_start') - def _compute_cash_balance(self): - for session in self: - cash_payment_method = session.payment_method_ids.filtered('is_cash_count')[:1] - if cash_payment_method: - total_cash_payment = 0.0 - captured_cash_payments_domain = Domain.AND([session._get_captured_payments_domain(), [('payment_method_id', '=', cash_payment_method.id)]]) - result = self.env['pos.payment']._read_group(captured_cash_payments_domain, aggregates=['amount:sum']) - total_cash_payment = result[0][0] or 0.0 - if session.state == 'closed': - total_cash = session.cash_real_transaction + total_cash_payment - else: - total_cash = sum(session.statement_line_ids.mapped('amount')) + total_cash_payment - - session.cash_register_balance_end = session.cash_register_balance_start + total_cash - session.cash_register_difference = session.cash_register_balance_end_real - session.cash_register_balance_end - else: - session.cash_register_balance_end = 0.0 - session.cash_register_difference = 0.0 - @api.depends('order_ids.payment_ids.amount') def _compute_total_payments_amount(self): result = self.env['pos.payment']._read_group(self._get_captured_payments_domain(), ['session_id'], ['amount:sum']) @@ -264,35 +306,32 @@ def _compute_total_payments_amount(self): for session in self: session.total_payments_amount = session_amount_map.get(session.id) or 0 + def _search_move_ids(self, operator, value): + moves = self.env['account.move'].search([('id', operator, value)]) + return [ + '|', + ('sales_move_id', 'in', moves.ids), + ('refunds_move_id', 'in', moves.ids), + ] + + @api.depends('sales_move_id', 'refunds_move_id') + def _compute_move_ids(self): + for session in self: + session.move_ids = session.sales_move_id | session.refunds_move_id + def _compute_order_count(self): orders_data = self.env['pos.order']._read_group([('session_id', 'in', self.ids)], ['session_id'], ['__count']) sessions_data = {session.id: count for session, count in orders_data} for session in self: session.order_count = sessions_data.get(session.id, 0) - @api.depends('cash_journal_id') - def _compute_cash_control(self): - # Only one cash register is supported by point_of_sale. - for session in self: - if session.cash_journal_id: - session.cash_control = session.config_id.cash_control - else: - session.cash_control = False - - @api.depends('config_id', 'payment_method_ids') - def _compute_cash_journal(self): - # Only one cash register is supported by point_of_sale. - for session in self: - cash_journal = session.payment_method_ids.filtered('is_cash_count')[:1].journal_id - session.cash_journal_id = cash_journal - @api.constrains('config_id') def _check_pos_config(self): onboarding_creation = self.env.context.get('onboarding_creation', False) if not onboarding_creation and self.search_count([ ('state', '!=', 'closed'), ('config_id', '=', self.config_id.id), - ('rescue', '=', False) + ('rescue', '=', False), ]) > 1: raise ValidationError(_("Another session is already opened for this point of sale.")) @@ -307,14 +346,6 @@ def _check_start_date(self): raise ValidationError(_("You cannot create a session starting before: %(lock_date_info)s", lock_date_info=self.env['res.company']._format_lock_dates(violated_lock_dates))) - def _check_invoices_are_posted(self): - unposted_invoices = self._get_closed_orders().sudo().with_company(self.company_id).account_move.filtered(lambda x: x.state != 'posted') - if unposted_invoices: - raise UserError(_( - 'You cannot close the POS when invoices are not posted.\nInvoices: %s', - '\n'.join(f'{invoice.name} - {invoice.state}' for invoice in unposted_invoices) - )) - @api.model_create_multi def create(self, vals_list): for vals in vals_list: @@ -326,7 +357,6 @@ def create(self, vals_list): # exists at the installation. If nothing is configured at the # installation we do the minimal configuration. Impossible to do in # the .xml files as the CoA is not yet installed. - vals.update(self._get_default_session_vals(config_id)) if self.env.user.has_group('point_of_sale.group_pos_user'): @@ -334,169 +364,36 @@ def create(self, vals_list): else: sessions = super().create(vals_list) - sessions.action_pos_session_open() return sessions @api.model def _get_default_session_vals(self, config_id): return {'config_id': config_id} - def unlink(self): - self.statement_line_ids.unlink() - return super().unlink() - - def action_pos_session_open(self): - # we only open sessions that haven't already been opened - for session in self.filtered(lambda session: session.state == 'opening_control'): - values = {} - if session.config_id.cash_control and not session.rescue: - last_session = self.search([('config_id', '=', session.config_id.id), ('id', '!=', session.id)], limit=1) - session.cash_register_balance_start = last_session.cash_register_balance_end_real # defaults to 0 if lastsession is empty - session.write(values) - return True - def get_session_orders(self): return self.order_ids.filtered(lambda o: - not (o.preset_time and o.preset_time.date() > fields.Date.today()) + not (o.preset_time and o.preset_time.date() > fields.Date.today()), ) - def action_pos_session_closing_control(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None): - bank_payment_method_diffs = bank_payment_method_diffs or {} - for session in self: - if any(order.state == 'draft' for order in self.get_session_orders()): - raise UserError(_("You cannot close the POS while there are still draft orders for the day.")) - if session.state == 'closed': - raise UserError(_('This session is already closed.')) - stop_at = self.stop_at or fields.Datetime.now() - session.write({'state': 'closing_control', 'stop_at': stop_at}) - if not session.config_id.cash_control: - return session.action_pos_session_close(balancing_account, amount_to_balance, bank_payment_method_diffs) - # If the session is in rescue, we only compute the payments in the cash register - # It is not yet possible to close a rescue session through the front end, see `close_session_from_ui` - if session.rescue and session.config_id.cash_control: - default_cash_payment_method_id = self.payment_method_ids.filtered(lambda pm: pm.type == 'cash')[0] - orders = self._get_closed_orders() - total_cash = sum( - orders.payment_ids.filtered(lambda p: p.payment_method_id == default_cash_payment_method_id).mapped('amount') - ) + self.cash_register_balance_start - - session.cash_register_balance_end_real = total_cash - - return session.action_pos_session_validate(balancing_account, amount_to_balance, bank_payment_method_diffs) - - - def action_pos_session_validate(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None): - bank_payment_method_diffs = bank_payment_method_diffs or {} - return self.action_pos_session_close(balancing_account, amount_to_balance, bank_payment_method_diffs) - - def action_pos_session_close(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None): - bank_payment_method_diffs = bank_payment_method_diffs or {} - # Session without cash payment method will not have a cash register. - # However, there could be other payment methods, thus, session still - # needs to be validated. - return self._validate_session(balancing_account, amount_to_balance, bank_payment_method_diffs) - - def _validate_session(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None): - self.ensure_one() - if self.get_session_orders().filtered(lambda o: o.state != 'cancel') or self.sudo().statement_line_ids: - self.cash_real_transaction = sum(self.sudo().statement_line_ids.mapped('amount')) - if self.state == 'closed': - raise UserError(_('This session is already closed.')) - self._check_if_no_draft_orders() - self._check_invoices_are_posted() - self._process_session_validation(balancing_account, amount_to_balance, bank_payment_method_diffs) - else: - self.sudo()._post_statement_difference(self.cash_register_difference) - - if self.config_id.order_edit_tracking: - edited_orders = self.get_session_orders().filtered(lambda o: o.is_edited) - if len(edited_orders) > 0: - body = _("Edited order(s) during the session:%s", - Markup("
    %s
") % Markup().join(Markup("
  • %s
  • ") % order._get_html_link() for order in edited_orders) - ) - self.message_post(body=body) - self.write({'state': 'closed'}) - self.env.flush_all() # ensure sale.report is up to date - return True - - def _process_session_validation(self, balancing_account, amount_to_balance, bank_payment_method_diffs): - bank_payment_method_diffs = bank_payment_method_diffs or {} - record = self.ensure_one() - if self.env.user.has_group('point_of_sale.group_pos_user'): - record = record.sudo() - cash_difference_before_statements = self.cash_register_difference - # when the user is POS, update the record in sudo - data = record.with_company(record.company_id).with_context( - check_move_validity=False, skip_invoice_sync=True - )._create_account_move(balancing_account, amount_to_balance, bank_payment_method_diffs) or {} - balance = sum(record.move_id.line_ids.mapped('balance')) + def _close_session_action(self): try: - with self.move_id._check_balanced({'records': self.move_id.sudo()}): - pass + self_ctx = self.with_company(self.company_id) + amount_residual = self_ctx._validate_session_accounting( + check_validity=False, + ) except UserError: - # Creating the account move is just part of a big database transaction - # when closing a session. There are other database changes that will happen - # before attempting to create the account move, such as, creating the picking - # records. - # We don't, however, want them to be committed when the account move creation - # failed; therefore, we need to roll back this transaction before showing the - # close session wizard. + # Rollback the transaction to avoid any change in the database + # as we just want to check the residual amount to balance self.env.cr.rollback() - return self._close_session_action(balance) - self.sudo()._post_statement_difference(cash_difference_before_statements) - if record.move_id.line_ids: - record.move_id.with_company(self.company_id)._post() - # Set the uninvoiced orders' state to 'done' - self.env['pos.order'].search([('session_id', '=', self.id), ('state', '=', 'paid')]).write({'state': 'done'}) - else: - record.move_id.sudo().unlink() - self.sudo().with_company(self.company_id)._reconcile_account_move_lines(data) - - def _post_statement_difference(self, amount): - if amount: - if self.config_id.cash_control: - st_line_vals = { - 'journal_id': self.cash_journal_id.id, - 'amount': amount, - 'date': self.statement_line_ids.sorted()[-1:].date or fields.Date.context_today(self), - 'pos_session_id': self.id, - } - - if amount < 0.0: - if not self.cash_journal_id.loss_account_id: - raise UserError( - _('Please go on the %s journal and define a Loss Account. This account will be used to record cash difference.', - self.cash_journal_id.name)) - - st_line_vals['payment_ref'] = _("Cash difference observed during the counting (Loss) - closing") - st_line_vals['counterpart_account_id'] = self.cash_journal_id.loss_account_id.id - else: - # self.cash_register_difference > 0.0 - if not self.cash_journal_id.profit_account_id: - raise UserError( - _('Please go on the %s journal and define a Profit Account. This account will be used to record cash difference.', - self.cash_journal_id.name)) - - st_line_vals['payment_ref'] = _("Cash difference observed during the counting (Profit) - closing") - st_line_vals['counterpart_account_id'] = self.cash_journal_id.profit_account_id.id - created_line = self.env['account.bank.statement.line'].with_context(no_retrieve_partner=True).create(st_line_vals) - - if created_line: - created_line.move_id.message_post(body=_( - "Related Session: %(link)s", - link=self._get_html_link() - )) - - def _close_session_action(self, amount_to_balance): - # NOTE This can't handle `bank_payment_method_diffs` because there is no field in the wizard that can carry it. default_account = self._get_balancing_account() wizard = self.env['pos.close.session.wizard'].create({ - 'amount_to_balance': amount_to_balance, + 'amount_to_balance': amount_residual, 'account_id': default_account.id, 'account_readonly': not self.env.user.has_group('account.group_account_readonly'), - 'message': _("There is a difference between the amounts to post and the amounts of the orders, it is probably caused by taxes or accounting configurations changes.") + 'message': _("There is a difference between the amounts to post and the amounts of the orders, it is probably caused by taxes or accounting configurations changes."), }) + return { 'name': _("Force Close Session"), 'type': 'ir.actions.act_window', @@ -507,162 +404,115 @@ def _close_session_action(self, amount_to_balance): 'context': {**self.env.context, 'active_ids': self.ids, 'active_model': 'pos.session'}, } - def close_session_from_ui(self, bank_payment_method_diff_pairs=None): - """Calling this method will try to close the session. + def action_pos_session_closing_control(self): + """ + This method is called in pos.session form view by clicking on + close session button, this happen when an error occurs during + the closing of the session from the UI. - param bank_payment_method_diff_pairs: list[(int, float)] - Pairs of payment_method_id and diff_amount which will be used to post - loss/profit when closing the session. + DO NOT CALL THIS METHOD FROM THE UI ! + """ + self.ensure_one() + result = self.close_session_from_ui() + if not result['status'] and result['type'] == 'accounting_error': + self.env.cr.rollback() + # Rollback to avoid any change in the database as we just + # want to show the error message and maybe open the reconciliation widget + return self._close_session_action() + return True - If successful, it returns {'successful': True} - Otherwise, it returns {'successful': False, 'message': str, 'redirect': bool}. - 'redirect' is a boolean used to know whether we redirect the user to the back end or not. - When necessary, error (i.e. UserError, AccessError) is raised which should redirect the user to the back end. + def close_session_from_ui(self, payment_method_closing={}): + """ + Main entry point for closing a session from the UI. It will + perform all necessary checks and operations to close the session """ - bank_payment_method_diffs = dict(bank_payment_method_diff_pairs or []) self.ensure_one() - # Even if this is called in `post_closing_cash_details`, we need to call this here too for case - # where cash_control = False - open_order_ids = self.get_session_orders().filtered(lambda o: o.state == 'draft').ids - check_closing_session = self._cannot_close_session(bank_payment_method_diffs) + if any(order.state == 'draft' for order in self.get_session_orders()): + return { + 'status': False, + 'type': 'draft_orders', + 'message': _("You cannot close the POS while there are still draft orders for the day."), + 'redirect': False, + } - if check_closing_session: - check_closing_session['open_order_ids'] = open_order_ids - return check_closing_session + if self.state == 'closed': + return { + 'status': False, + 'type': 'session_already_closed', + 'message': _("This session is already closed."), + 'redirect': True, + } self.config_id.close_session_snoozes() - - future_orders = self.order_ids.filtered(lambda order: order.preset_time and order.preset_time.date() > fields.Date.today() and order.state == 'draft') + future_orders = self.order_ids.filtered_domain([ + ('preset_time', '!=', False), + ('preset_time', '>', fields.Datetime.now()), + ('state', '=', 'draft'), + ]) future_orders.session_id = False - validate_result = self.action_pos_session_closing_control(bank_payment_method_diffs=bank_payment_method_diffs) + self.write({ + 'state': 'closing_control', + 'stop_at': fields.Datetime.now(), + }) - # If an error is raised, the user will still be redirected to the back end to manually close the session. - # If the return result is a dict, this means that normally we have a redirection or a wizard => we redirect the user - if isinstance(validate_result, dict): - # imbalance accounting entry + try: + self_ctx = self.with_company(self.company_id) + amount_residual = self_ctx._validate_session_accounting() + except UserError as e: + self.env.cr.rollback() return { - 'open_order_ids': open_order_ids, - 'successful': False, - 'message': validate_result.get('name'), - 'redirect': True + 'status': False, + 'type': 'accounting_error', + 'redirect': True, + 'data': {}, + 'message': str(e), } - if self.env.user.email: - self.post_close_register_message() - return {'successful': True} - - def post_close_register_message(self): - self.message_post(body=_('Closed Register')) - - def update_closing_control_state_session(self, notes): - # Prevent closing the session again if it was already closed - if self.state == 'closed': - raise UserError(_('This session is already closed.')) - # Prevent the session to be opened again. - self.write({'state': 'closing_control', 'stop_at': fields.Datetime.now(), 'closing_notes': notes}) - self._post_cash_details_message('Closing', self.cash_register_balance_end, self.cash_register_difference, notes) - - def post_closing_cash_details(self, counted_cash): - """ - Calling this method will try store the cash details during the session closing. - - :param counted_cash: float, the total cash the user counted from its cash register - - If successful, it returns ``{'successful': True}``. - Otherwise, it returns ``{'successful': False, 'message': str, 'redirect': bool}`` where - ``'redirect'`` is a boolean used to know whether we redirect the user to the back end or not. - When necessary, error (i.e. UserError, AccessError) is raised which should redirect the user to the back end. - """ - self.ensure_one() - check_closing_session = self._cannot_close_session() - if check_closing_session: - open_order_ids = self.get_session_orders().filtered(lambda o: o.state == 'draft').ids - check_closing_session['open_order_ids'] = open_order_ids - return check_closing_session - - if not self.cash_journal_id: - # The user is blocked anyway, this user error is mostly for developers that try to call this function - raise UserError(_("There is no cash register in this session.")) - - self.cash_register_balance_end_real = counted_cash - - return {'successful': True} - - def _create_diff_account_move_for_split_payment_method(self, payment_method, diff_amount): - self.ensure_one() - - get_diff_vals_result = self._get_diff_vals(payment_method.id, diff_amount) - if not get_diff_vals_result: - return + self._handle_bank_payment_method_difference(payment_method_closing) + self._handle_cash_statement_entries(payment_method_closing) - source_vals, dest_vals = get_diff_vals_result - diff_move = self.env['account.move'].create({ - 'journal_id': payment_method.journal_id.id, - 'date': fields.Date.context_today(self), - 'ref': self._get_diff_account_move_ref(payment_method), - 'line_ids': [Command.create(source_vals), Command.create(dest_vals)] - }) - diff_move._post() + statement = self.bank_statement_id + if statement: + statement._compute_balance_end_real() - def _get_diff_account_move_ref(self, payment_method): - return _('Closing difference in %(payment_method)s (%(session)s)', payment_method=payment_method.name, session=self.name) + if not float_is_zero(amount_residual, precision_rounding=self.currency_id.rounding): + formatted = self.currency_id.format(amount_residual) + return { + 'status': False, + 'type': 'accounting_error', + 'redirect': True, + 'data': { + 'amount_residual': amount_residual, + }, + 'message': _( + "The session cannot be closed due to an accounting" + " difference of %s. Please review the journal entries" + " and try again.", formatted), + } - def _get_diff_vals(self, payment_method_id, diff_amount, outstanding_account=False): - payment_method = self.env['pos.payment.method'].browse(payment_method_id) - diff_compare_to_zero = self.currency_id.compare_amounts(diff_amount, 0) - source_account = payment_method.outstanding_account_id or outstanding_account - destination_account = self.env['account.account'] + if self.config_id.order_edit_tracking: + edited_orders = self.get_session_orders().filtered(lambda o: o.is_edited) + if len(edited_orders) > 0: + order_links = Markup().join( + Markup("
  • %s
  • ") % order._get_html_link() for order in edited_orders + ) + body = _( + "Edited order(s) during the session:%s", + Markup("
      %s
    ") % order_links, + ) + self.message_post(body=body) - if (diff_compare_to_zero > 0): - destination_account = payment_method.journal_id.profit_account_id - elif (diff_compare_to_zero < 0): - destination_account = payment_method.journal_id.loss_account_id + if self.env.user.email: + self.post_close_register_message() - if (diff_compare_to_zero == 0 or not source_account): - return False + self.write({'state': 'closed'}) + self.order_ids.write({'state': 'done'}) + self.env.flush_all() # ensure sale.report is up to date - amounts = self._update_amounts({'amount': 0, 'amount_converted': 0}, {'amount': diff_amount}, self.stop_at) - source_vals = self._debit_amounts({'account_id': source_account.id}, amounts['amount'], amounts['amount_converted']) - dest_vals = self._credit_amounts({'account_id': destination_account.id}, amounts['amount'], amounts['amount_converted']) - return [source_vals, dest_vals] + return {'status': True} - def _cannot_close_session(self, bank_payment_method_diffs=None): - """ - Add check in this method if you want to return or raise an error when trying to either post cash details - or close the session. Raising an error will always redirect the user to the back end. - It should return {'successful': False, 'message': str, 'redirect': bool} if we can't close the session - """ - bank_payment_method_diffs = bank_payment_method_diffs or {} - if any(order.state == 'draft' for order in self.get_session_orders()): - return {'successful': False, 'message': _("You cannot close the POS while there are still draft orders for the day."), 'redirect': False} - if self.state == 'closed': - return { - 'successful': False, - 'type': 'alert', - 'title': 'Session already closed', - 'message': _("The session has been already closed by another User. " - "All sales completed in the meantime have been saved in a " - "Rescue Session, which can be reviewed anytime and posted " - "to Accounting from Point of Sale's dashboard."), - 'redirect': True - } - if bank_payment_method_diffs: - no_loss_account = self.env['account.journal'] - no_profit_account = self.env['account.journal'] - for payment_method in self.env['pos.payment.method'].browse(bank_payment_method_diffs.keys()): - journal = payment_method.journal_id - compare_to_zero = self.currency_id.compare_amounts(bank_payment_method_diffs.get(payment_method.id), 0) - if compare_to_zero == -1 and not journal.loss_account_id: - no_loss_account |= journal - elif compare_to_zero == 1 and not journal.profit_account_id: - no_profit_account |= journal - message = '' - if no_loss_account: - message += _("Need loss account for the following journals to post the lost amount: %s\n", ', '.join(no_loss_account.mapped('name'))) - if no_profit_account: - message += _("Need profit account for the following journals to post the gained amount: %s", ', '.join(no_profit_account.mapped('name'))) - if message: - return {'successful': False, 'message': message, 'redirect': False} + def post_close_register_message(self): + self.message_post(body=_('Closed Register')) def get_cash_in_out_list(self): if not self.env.user.has_group('point_of_sale.group_pos_user'): @@ -670,7 +520,7 @@ def get_cash_in_out_list(self): cash_in_count = 0 cash_out_count = 0 cash_in_out_list = [] - for cash_move in self.sudo().statement_line_ids.sorted('create_date'): + for cash_move in self.sudo().bank_statement_line_ids.sorted('create_date'): if cash_move.amount > 0: cash_in_count += 1 name = f'Cash in {cash_in_count}' @@ -692,31 +542,32 @@ def get_closing_control_data(self): self.ensure_one() orders = self._get_closed_orders() payments = orders.payment_ids.filtered(lambda p: p.payment_method_id.type != "pay_later") - cash_payment_method_ids = self.payment_method_ids.filtered(lambda pm: pm.type == 'cash') - default_cash_payment_method_id = cash_payment_method_ids[0] if cash_payment_method_ids else None - default_cash_payments = payments.filtered(lambda p: p.payment_method_id == default_cash_payment_method_id) if default_cash_payment_method_id else [] - total_default_cash_payment_amount = sum(default_cash_payments.mapped('amount')) if default_cash_payment_method_id else 0 - non_cash_payment_method_ids = self.payment_method_ids - default_cash_payment_method_id if default_cash_payment_method_id else self.payment_method_ids + cash_pm = self.config_id._get_cash_payment_method() + cash_payments = payments.filtered_domain([ + ('payment_method_id', '=', cash_pm.id), + ('pos_order_id.is_singly_invoiced', '=', False), + ]) + cash_payments_summary = sum(cash_payments.mapped('amount')) + non_cash_payment_method_ids = self.payment_method_ids - cash_pm non_cash_payments_grouped_by_method_id = {pm: orders.payment_ids.filtered(lambda p: p.payment_method_id == pm) for pm in non_cash_payment_method_ids} - + ending_cash_balance = self.bank_statement_id.balance_end or 0 cash_in_out_list = self.get_cash_in_out_list() + opening_amount = self.config_id._get_opening_balance() return { 'orders_details': { 'quantity': len(orders), - 'amount': sum(orders.mapped('amount_total')) + 'amount': sum(orders.mapped('amount_total')), }, 'opening_notes': self.opening_notes, 'default_cash_details': { - 'name': default_cash_payment_method_id.name, - 'amount': self.cash_register_balance_start - + total_default_cash_payment_amount - + sum(self.sudo().statement_line_ids.mapped('amount')), - 'opening': self.cash_register_balance_start, - 'payment_amount': total_default_cash_payment_amount, + 'name': cash_pm.name, + 'amount': ending_cash_balance + cash_payments_summary, + 'opening': opening_amount, + 'payment_amount': cash_payments_summary, 'moves': cash_in_out_list, - 'id': default_cash_payment_method_id.id - } if default_cash_payment_method_id else {}, + 'id': cash_pm.id, + } if cash_pm else {}, 'non_cash_payment_methods': [{ 'name': pm.name, 'amount': sum(non_cash_payments_grouped_by_method_id[pm].mapped('amount')), @@ -725,746 +576,16 @@ def get_closing_control_data(self): 'type': pm.type, } for pm in non_cash_payment_method_ids], 'is_manager': self.env.user.has_group("point_of_sale.group_pos_manager"), - 'amount_authorized_diff': self.config_id.amount_authorized_diff if self.config_id.set_maximum_difference else None + 'amount_authorized_diff': self.config_id.amount_authorized_diff if self.config_id.set_maximum_difference else None, } - def _create_balancing_line(self, data, balancing_account, amount_to_balance): - if not self.company_id.currency_id.is_zero(amount_to_balance): - balancing_vals = self._prepare_balancing_line_vals(amount_to_balance, self.move_id, balancing_account) - MoveLine = data.get('MoveLine') - MoveLine.create(balancing_vals) - return data - - def _prepare_balancing_line_vals(self, imbalance_amount, move, balancing_account): - partial_vals = { - 'name': _('Difference at closing PoS session'), - 'account_id': balancing_account.id, - 'move_id': move.id, - 'partner_id': False, - } - # `imbalance_amount` is already in terms of company currency so it is the amount_converted - # param when calling `_credit_amounts`. amount param will be the converted value of - # `imbalance_amount` from company currency to the session currency. - imbalance_amount_session = 0 - if (not self.is_in_company_currency): - imbalance_amount_session = self.company_id.currency_id._convert(imbalance_amount, self.currency_id, self.company_id, fields.Date.context_today(self)) - return self._credit_amounts(partial_vals, imbalance_amount_session, imbalance_amount) - def _get_balancing_account(self): - return ( - self.company_id.account_default_pos_receivable_account_id - or self.env['res.partner']._fields['property_account_receivable_id'].get_company_dependent_fallback(self.env['res.partner']) - or self.env['account.account'] - ) - - def _create_account_move(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None): - """ Create account.move and account.move.line records for this session. - - Side-effects include: - - setting self.move_id to the created account.move record - - reconciling cash receivable lines, invoice receivable lines and stock output lines - """ - account_move = self.env['account.move'].create({ - 'journal_id': self.config_id.journal_id.id, - 'date': fields.Date.context_today(self), - 'ref': self.name, - }) - self.write({'move_id': account_move.id}) - data = self._get_account_move_data(bank_payment_method_diffs) - if balancing_account and amount_to_balance: - data = self._create_balancing_line(data, balancing_account, amount_to_balance) - return data - - def _get_account_move_data(self, bank_payment_method_diffs): - data = {'bank_payment_method_diffs': bank_payment_method_diffs or {}} - data = self._accumulate_amounts(data) - data = self._create_non_reconciliable_move_lines(data) - data = self._create_bank_payment_moves(data) - data = self._create_pay_later_receivable_lines(data) - data = self._create_cash_statement_lines_and_cash_move_lines(data) - data = self._create_invoice_receivable_lines(data) - return data - - def _accumulate_amounts(self, data): - # Accumulate the amounts for each accounting lines group - # Each dict maps `key` -> `amounts`, where `key` is the group key. - # E.g. `combine_receivables_bank` is derived from pos.payment records - # in the self.order_ids with group key of the `payment_method_id` - # field of the pos.payment record. - AccountTax = self.env['account.tax'] - amounts = lambda: {'amount': 0.0, 'amount_converted': 0.0} - tax_amounts = lambda: {'amount': 0.0, 'amount_converted': 0.0, 'base_amount': 0.0, 'base_amount_converted': 0.0} - split_receivables_bank = defaultdict(amounts) - split_receivables_cash = defaultdict(amounts) - split_receivables_pay_later = defaultdict(amounts) - combine_receivables_bank = defaultdict(amounts) - combine_receivables_cash = defaultdict(amounts) - combine_receivables_pay_later = defaultdict(amounts) - combine_invoice_receivables = defaultdict(amounts) - split_invoice_receivables = defaultdict(amounts) - sales = defaultdict(amounts) - taxes = defaultdict(tax_amounts) - rounding_difference = {'amount': 0.0, 'amount_converted': 0.0} - # Track the receivable lines of the order's invoice payment moves for reconciliation - # These receivable lines are reconciled to the corresponding invoice receivable lines - # of this session's move_id. - combine_inv_payment_receivable_lines = defaultdict(lambda: self.env['account.move.line']) - split_inv_payment_receivable_lines = defaultdict(lambda: self.env['account.move.line']) - pos_receivable_account = self.company_id.account_default_pos_receivable_account_id - currency_rounding = self.currency_id.rounding - closed_orders = self._get_closed_orders() - for order in closed_orders: - order_is_invoiced = order.is_invoiced - for payment in order.payment_ids: - amount = payment.amount - if float_is_zero(amount, precision_rounding=currency_rounding): - continue - date = payment.payment_date - payment_method = payment.payment_method_id - is_split_payment = payment.payment_method_id.split_transactions - payment_type = payment_method.type - - # If not pay_later, we create the receivable vals for both invoiced and uninvoiced orders. - # Separate the split and aggregated payments. - # Moreover, if the order is invoiced, we create the pos receivable vals that will balance the - # pos receivable lines from the invoice payments. - if payment_type != 'pay_later': - if is_split_payment and payment_type == 'cash': - split_receivables_cash[payment] = self._update_amounts(split_receivables_cash[payment], {'amount': amount}, date) - elif not is_split_payment and payment_type == 'cash': - combine_receivables_cash[payment_method] = self._update_amounts(combine_receivables_cash[payment_method], {'amount': amount}, date) - elif is_split_payment and payment_type == 'bank': - split_receivables_bank[payment] = self._update_amounts(split_receivables_bank[payment], {'amount': amount}, date) - elif not is_split_payment and payment_type == 'bank': - combine_receivables_bank[payment_method] = self._update_amounts(combine_receivables_bank[payment_method], {'amount': amount}, date) - - # Create the vals to create the pos receivables that will balance the pos receivables from invoice payment moves. - if order_is_invoiced: - if is_split_payment: - split_inv_payment_receivable_lines[payment] |= payment.account_move_id.line_ids.filtered(lambda line: line.account_id == pos_receivable_account) - split_invoice_receivables[payment] = self._update_amounts(split_invoice_receivables[payment], {'amount': payment.amount}, order.date_order) - else: - combine_inv_payment_receivable_lines[payment_method] |= payment.account_move_id.line_ids.filtered(lambda line: line.account_id == pos_receivable_account) - combine_invoice_receivables[payment_method] = self._update_amounts(combine_invoice_receivables[payment_method], {'amount': payment.amount}, order.date_order) - - # If pay_later, we create the receivable lines. - # if split, with partner - # Otherwise, it's aggregated (combined) - # But only do if order is *not* invoiced because no account move is created for pay later invoice payments. - if payment_type == 'pay_later' and not order_is_invoiced: - if is_split_payment: - split_receivables_pay_later[payment] = self._update_amounts(split_receivables_pay_later[payment], {'amount': amount}, date) - elif not is_split_payment: - combine_receivables_pay_later[payment_method] = self._update_amounts(combine_receivables_pay_later[payment_method], {'amount': amount}, date) - - if not order_is_invoiced: - base_lines = order.with_context(linked_to_pos=True)._prepare_tax_base_line_values() - AccountTax._add_tax_details_in_base_lines(base_lines, order.company_id) - AccountTax._round_base_lines_tax_details(base_lines, order.company_id) - AccountTax._add_accounting_data_in_base_lines_tax_details(base_lines, order.company_id, include_caba_tags=True) - tax_results = AccountTax._prepare_tax_lines(base_lines, order.company_id) - total_amount_currency = 0.0 - for base_line, to_update in tax_results['base_lines_to_update']: - # Combine sales/refund lines - sale_vals_dict = self._get_sale_key(base_line) - sale_key = frozendict(sale_vals_dict) - total_amount_currency += to_update['amount_currency'] - sales[sale_key] = self._update_amounts( - sales[sale_key], - { - 'amount': to_update['amount_currency'], - 'amount_converted': to_update['balance'], - }, - order.date_order, - ) - if self.config_id._is_quantities_set(): - sales[sale_key].setdefault('quantity', 0) - sales[sale_key]['quantity'] += base_line['quantity'] - - # Combine tax lines - for tax_line in tax_results['tax_lines_to_add']: - tax_key = ( - tax_line['account_id'], - tax_line['tax_repartition_line_id'], - tuple(tax_line['tax_tag_ids'][0][2]), - ) - total_amount_currency += tax_line['amount_currency'] - taxes[tax_key] = self._update_amounts( - taxes[tax_key], - { - 'amount': tax_line['amount_currency'], - 'amount_converted': tax_line['balance'], - 'base_amount': tax_line['tax_base_amount'] - }, - order.date_order, - ) - - if self.config_id.cash_rounding: - diff = order.amount_paid + total_amount_currency - rounding_difference = self._update_amounts(rounding_difference, {'amount': diff}, order.date_order) - - # Increasing current partner's customer_rank - partners = (order.partner_id | order.partner_id.commercial_partner_id) - partners._increase_rank('customer_rank') - - MoveLine = self.env['account.move.line'].with_context(check_move_validity=False, skip_invoice_sync=True) - - data.update({ - 'taxes': taxes, - 'sales': sales, - 'split_receivables_bank': split_receivables_bank, - 'combine_receivables_bank': combine_receivables_bank, - 'split_receivables_cash': split_receivables_cash, - 'combine_receivables_cash': combine_receivables_cash, - 'combine_invoice_receivables': combine_invoice_receivables, - 'split_receivables_pay_later': split_receivables_pay_later, - 'combine_receivables_pay_later': combine_receivables_pay_later, - 'combine_inv_payment_receivable_lines': combine_inv_payment_receivable_lines, - 'rounding_difference': rounding_difference, - 'MoveLine': MoveLine, - 'split_invoice_receivables': split_invoice_receivables, - 'split_inv_payment_receivable_lines': split_inv_payment_receivable_lines, - }) - return data - - def _create_non_reconciliable_move_lines(self, data): - # Create account.move.line records for - # - sales - # - taxes - # - non-cash split receivables (not for automatic reconciliation) - # - non-cash combine receivables (not for automatic reconciliation) - taxes = data.get('taxes') - sales = data.get('sales') - MoveLine = data.get('MoveLine') - - tax_vals = [self._get_tax_vals(key, amounts['amount'], amounts['amount_converted'], amounts['base_amount_converted']) for key, amounts in taxes.items()] - # Check if all taxes lines have account_id assigned. If not, there are repartition lines of the tax that have no account_id. - tax_names_no_account = [line['name'] for line in tax_vals if not line['account_id']] - if tax_names_no_account: - raise UserError(_( - 'Unable to close and validate the session.\n' - 'Please set corresponding tax account in each repartition line of the following taxes: \n%s', - ', '.join(tax_names_no_account) - )) - - MoveLine.create(tax_vals) - move_line_ids = MoveLine.create(list(starmap(self._get_sale_vals, sales.items()))) - for key, ml_id in zip(sales.keys(), move_line_ids.ids): - sales[key]['move_line_id'] = ml_id - - return data - - def _create_bank_payment_moves(self, data): - combine_receivables_bank = data.get('combine_receivables_bank') - split_receivables_bank = data.get('split_receivables_bank') - bank_payment_method_diffs = data.get('bank_payment_method_diffs') - MoveLine = data.get('MoveLine') - payment_method_to_receivable_lines = {} - payment_to_receivable_lines = {} - for payment_method, amounts in combine_receivables_bank.items(): - combine_receivable_line = MoveLine.create(self._get_combine_receivable_vals(payment_method, amounts['amount'], amounts['amount_converted'])) - payment_receivable_line = self._create_combine_account_payment(payment_method, amounts, diff_amount=bank_payment_method_diffs.get(payment_method.id) or 0) - payment_method_to_receivable_lines[payment_method] = combine_receivable_line | payment_receivable_line - - for payment, amounts in split_receivables_bank.items(): - split_receivable_line = MoveLine.create(self._get_split_receivable_vals(payment, amounts['amount'], amounts['amount_converted'])) - payment_receivable_line = self._create_split_account_payment(payment, amounts) - payment_to_receivable_lines[payment] = split_receivable_line | payment_receivable_line - - for bank_payment_method in self.payment_method_ids.filtered(lambda pm: pm.type == 'bank' and pm.split_transactions): - self._create_diff_account_move_for_split_payment_method(bank_payment_method, bank_payment_method_diffs.get(bank_payment_method.id) or 0) - - data['payment_method_to_receivable_lines'] = payment_method_to_receivable_lines - data['payment_to_receivable_lines'] = payment_to_receivable_lines - return data - - def _create_pay_later_receivable_lines(self, data): - MoveLine = data.get('MoveLine') - combine_receivables_pay_later = data.get('combine_receivables_pay_later') - split_receivables_pay_later = data.get('split_receivables_pay_later') - vals = [] - for payment_method, amounts in combine_receivables_pay_later.items(): - vals.append(self._get_combine_receivable_vals(payment_method, amounts['amount'], amounts['amount_converted'])) - for payment, amounts in split_receivables_pay_later.items(): - vals.append(self._get_split_receivable_vals(payment, amounts['amount'], amounts['amount_converted'])) - for val in vals: - # Entries related to a `pay_later` payment method should not be excluded from follow-ups. - val['no_followup'] = False - data['pay_later_move_lines'] = MoveLine.create(vals) - return data - - def _ensure_payment_outstanding_account(self, payment, payment_amount): - # In community the outstanding account is computed on the creation of account.payment records - if not payment.outstanding_account_id and self.env['account.move']._get_invoice_in_payment_state() == 'in_payment': - payment.outstanding_account_id = payment._get_outstanding_account(payment.payment_type) - - if float_compare(payment_amount, 0, precision_rounding=self.currency_id.rounding) < 0: - payment.write({ - 'force_outstanding_account_id': payment.destination_account_id, - 'destination_account_id': payment.outstanding_account_id, - 'payment_type': 'outbound', - }) - - def _create_combine_account_payment(self, payment_method, amounts, diff_amount): - outstanding_account = payment_method.outstanding_account_id - destination_account = self._get_receivable_account(payment_method) - - account_payment = self.env['account.payment'].with_context(pos_payment=True).create({ - 'amount': abs(amounts['amount']), - 'journal_id': payment_method.journal_id.id, - 'force_outstanding_account_id': outstanding_account.id, - 'destination_account_id': destination_account.id, - 'memo': _('Combine %(payment_method)s POS payments from %(session)s', payment_method=payment_method.name, session=self.name), - 'pos_payment_method_id': payment_method.id, - 'pos_session_id': self.id, - 'company_id': self.company_id.id, - }) - - self._ensure_payment_outstanding_account(account_payment, amounts['amount']) - account_payment.action_post() - - diff_amount_compare_to_zero = self.currency_id.compare_amounts(diff_amount, 0) - if diff_amount_compare_to_zero != 0: - self._apply_diff_on_account_payment_move(account_payment, payment_method, diff_amount) - - return account_payment.move_id.line_ids.filtered(lambda line: line.account_id == self._get_receivable_account(payment_method)) - - def _apply_diff_on_account_payment_move(self, account_payment, payment_method, diff_amount): - diff_vals = self._get_diff_vals(payment_method.id, diff_amount, account_payment.outstanding_account_id) - if not diff_vals: - return - source_vals, dest_vals = diff_vals - outstanding_line = account_payment.move_id.line_ids.filtered(lambda line: line.account_id.id == source_vals['account_id']) - new_balance = outstanding_line.balance + self._amount_converter(diff_amount, self.stop_at, False) - new_balance_compare_to_zero = self.currency_id.compare_amounts(new_balance, 0) - account_payment.move_id.button_draft() - account_payment.move_id.write({ - 'line_ids': [ - Command.create(dest_vals), - Command.update(outstanding_line.id, { - 'debit': new_balance_compare_to_zero > 0 and new_balance or 0.0, - 'credit': new_balance_compare_to_zero < 0 and -new_balance or 0.0 - }) - ] - }) - account_payment.write({ - 'amount': abs(new_balance), - }) - account_payment.move_id.action_post() - - def _create_split_account_payment(self, payment, amounts): - payment_method = payment.payment_method_id - if not payment_method.journal_id: - return self.env['account.move.line'] - outstanding_account = payment_method.outstanding_account_id - accounting_partner = self.env["res.partner"]._find_accounting_partner(payment.partner_id) - destination_account = accounting_partner.property_account_receivable_id - - account_payment = self.env['account.payment'].create({ - 'amount': abs(amounts['amount']), - 'partner_id': accounting_partner.id, - 'journal_id': payment_method.journal_id.id, - 'force_outstanding_account_id': outstanding_account.id, - 'destination_account_id': destination_account.id, - 'memo': _('%(payment_method)s POS payment of %(partner)s in %(session)s', payment_method=payment_method.name, partner=payment.partner_id.display_name, session=self.name), - 'pos_payment_method_id': payment_method.id, - 'pos_session_id': self.id, - }) - - self._ensure_payment_outstanding_account(account_payment, amounts['amount']) - account_payment.action_post() - return account_payment.move_id.line_ids.filtered(lambda line: line.account_id == accounting_partner.property_account_receivable_id) - - def _create_cash_statement_lines_and_cash_move_lines(self, data): - # Create the split and combine cash statement lines and account move lines. - # `split_cash_statement_lines` maps `journal` -> split cash statement lines - # `combine_cash_statement_lines` maps `journal` -> combine cash statement lines - # `split_cash_receivable_lines` maps `journal` -> split cash receivable lines - # `combine_cash_receivable_lines` maps `journal` -> combine cash receivable lines - MoveLine = data.get('MoveLine') - split_receivables_cash = data.get('split_receivables_cash') - combine_receivables_cash = data.get('combine_receivables_cash') - - # handle split cash payments - split_cash_statement_line_vals = [] - split_cash_receivable_vals = [] - for payment, amounts in split_receivables_cash.items(): - journal_id = payment.payment_method_id.journal_id - split_cash_statement_line_vals.append( - self._get_split_statement_line_vals( - journal_id, - amounts['amount'], - payment - ) - ) - split_cash_receivable_vals.append( - self._get_split_receivable_vals( - payment, - amounts['amount'], - amounts['amount_converted'] - ) - ) - # handle combine cash payments - combine_cash_statement_line_vals = [] - combine_cash_receivable_vals = [] - for payment_method, amounts in combine_receivables_cash.items(): - if not float_is_zero(amounts['amount'] , precision_rounding=self.currency_id.rounding): - combine_cash_statement_line_vals.append( - self._get_combine_statement_line_vals( - payment_method.journal_id, - amounts['amount'], - payment_method - ) - ) - combine_cash_receivable_vals.append( - self._get_combine_receivable_vals( - payment_method, - amounts['amount'], - amounts['amount_converted'] - ) - ) - - # create the statement lines and account move lines - BankStatementLine = self.env['account.bank.statement.line'].with_context(no_retrieve_partner=True) - split_cash_statement_lines = {} - combine_cash_statement_lines = {} - split_cash_receivable_lines = {} - combine_cash_receivable_lines = {} - split_cash_statement_lines = BankStatementLine.create(split_cash_statement_line_vals).mapped('move_id.line_ids').filtered(lambda line: line.account_id.account_type == 'asset_receivable') - combine_cash_statement_lines = BankStatementLine.create(combine_cash_statement_line_vals).mapped('move_id.line_ids').filtered(lambda line: line.account_id.account_type == 'asset_receivable') - split_cash_receivable_lines = MoveLine.create(split_cash_receivable_vals) - combine_cash_receivable_lines = MoveLine.create(combine_cash_receivable_vals) - - data.update( - {'split_cash_statement_lines': split_cash_statement_lines, - 'combine_cash_statement_lines': combine_cash_statement_lines, - 'split_cash_receivable_lines': split_cash_receivable_lines, - 'combine_cash_receivable_lines': combine_cash_receivable_lines - }) - return data - - def _create_invoice_receivable_lines(self, data): - # Create invoice receivable lines for this session's move_id. - # Keep reference of the invoice receivable lines because - # they are reconciled with the lines in combine_inv_payment_receivable_lines - MoveLine = data.get('MoveLine') - combine_invoice_receivables = data.get('combine_invoice_receivables') - split_invoice_receivables = data.get('split_invoice_receivables') - - combine_invoice_receivable_vals = defaultdict(list) - split_invoice_receivable_vals = defaultdict(list) - combine_invoice_receivable_lines = {} - split_invoice_receivable_lines = {} - for payment_method, amounts in combine_invoice_receivables.items(): - combine_invoice_receivable_vals[payment_method].append(self._get_invoice_receivable_vals(amounts['amount'], amounts['amount_converted'])) - for payment, amounts in split_invoice_receivables.items(): - split_invoice_receivable_vals[payment].append(self._get_invoice_receivable_vals(amounts['amount'], amounts['amount_converted'])) - for payment_method, vals in combine_invoice_receivable_vals.items(): - receivable_lines = MoveLine.create(vals) - combine_invoice_receivable_lines[payment_method] = receivable_lines - for payment, vals in split_invoice_receivable_vals.items(): - receivable_lines = MoveLine.create(vals) - split_invoice_receivable_lines[payment] = receivable_lines - - data.update({'combine_invoice_receivable_lines': combine_invoice_receivable_lines}) - data.update({'split_invoice_receivable_lines': split_invoice_receivable_lines}) - return data - - def _reconcile_account_move_lines(self, data): - # reconcile cash receivable lines - split_cash_statement_lines = data.get('split_cash_statement_lines') - combine_cash_statement_lines = data.get('combine_cash_statement_lines') - split_cash_receivable_lines = data.get('split_cash_receivable_lines') - combine_cash_receivable_lines = data.get('combine_cash_receivable_lines') - combine_inv_payment_receivable_lines = data.get('combine_inv_payment_receivable_lines') - split_inv_payment_receivable_lines = data.get('split_inv_payment_receivable_lines') - combine_invoice_receivable_lines = data.get('combine_invoice_receivable_lines') - split_invoice_receivable_lines = data.get('split_invoice_receivable_lines') - payment_method_to_receivable_lines = data.get('payment_method_to_receivable_lines') - payment_to_receivable_lines = data.get('payment_to_receivable_lines') - - all_lines = ( - split_cash_statement_lines - | combine_cash_statement_lines - | split_cash_receivable_lines - | combine_cash_receivable_lines - ) - all_lines.filtered(lambda line: line.move_id.state != 'posted').move_id._post(soft=False) - - lines_by_account = all_lines.filtered(lambda l: not l.reconciled).grouped('account_id') - for lines in lines_by_account.values(): - lines.with_context(no_cash_basis=True).reconcile() - - for payment_method, lines in payment_method_to_receivable_lines.items(): - lines.filtered(lambda line: not line.reconciled).with_context(no_cash_basis=True).reconcile() - - for payment, lines in payment_to_receivable_lines.items(): - lines.filtered(lambda line: not line.reconciled).with_context(no_cash_basis=True).reconcile() - - # Reconcile invoice payments' receivable lines. - for payment_method in combine_inv_payment_receivable_lines: - lines = combine_inv_payment_receivable_lines[payment_method] | combine_invoice_receivable_lines.get(payment_method, self.env['account.move.line']) - lines.filtered(lambda line: not line.reconciled).with_context(no_cash_basis=True).reconcile() - - for payment in split_inv_payment_receivable_lines: - lines = split_inv_payment_receivable_lines[payment] | split_invoice_receivable_lines.get(payment, self.env['account.move.line']) - lines.filtered(lambda line: not line.reconciled).with_context(no_cash_basis=True).reconcile() - - return data - - def _get_split_receivable_vals(self, payment, amount, amount_converted): - accounting_partner = self.env["res.partner"]._find_accounting_partner(payment.partner_id) - if not accounting_partner: - raise UserError(_("You have enabled the \"Identify Customer\" option for %(payment_method)s payment method," - "but the order %(order)s does not contain a customer.", - payment_method=payment.payment_method_id.name, - order=payment.pos_order_id.name)) - partial_vals = { - 'account_id': accounting_partner.property_account_receivable_id.id, - 'move_id': self.move_id.id, - 'partner_id': accounting_partner.id, - 'name': '%s - %s' % (self.name, payment.payment_method_id.name), - } - return self._debit_amounts(partial_vals, amount, amount_converted) - - def _get_combine_receivable_vals(self, payment_method, amount, amount_converted): - partial_vals = { - 'account_id': self._get_receivable_account(payment_method).id, - 'move_id': self.move_id.id, - 'name': '%s - %s' % (self.name, payment_method.name), - 'display_type': 'payment_term', - } - return self._debit_amounts(partial_vals, amount, amount_converted) - - def _get_invoice_receivable_vals(self, amount, amount_converted): - partial_vals = { - 'account_id': self.company_id.account_default_pos_receivable_account_id.id, - 'move_id': self.move_id.id, - 'name': _('From invoice payments'), - 'display_type': 'payment_term', - } - return self._credit_amounts(partial_vals, amount, amount_converted) - - def _get_sale_key(self, base_line): - return { - # account - 'account_id': base_line['account_id'].id, - # sign - 'sign': -1 if base_line['is_refund'] else 1, - # for taxes - 'tax_ids': tuple(base_line['record'].tax_ids_after_fiscal_position.flatten_taxes_hierarchy().ids), - 'base_tag_ids': tuple(base_line['tax_tag_ids'].ids), - 'product_id': base_line['product_id'].id if self.config_id.use_closing_entry_by_product else False, - } - - def _get_sale_vals(self, key, sale_vals): - tax_ids = key['tax_ids'] - product_id = key['product_id'] - sign = key['sign'] - applied_taxes = self.env['account.tax'].browse(tax_ids) - if product_id: - product = self.env['product.product'].browse(product_id) - product_name = product.display_name - product_uom = product.uom_id.id - else: - product_name = "" - product_uom = False - title = _('Sales') if sign == 1 else _('Refund') - name = _('%s untaxed', title) - if applied_taxes: - name = _('%(title)s %(product_name)s with %(taxes)s', title=title, product_name=product_name, taxes=', '.join([tax.name for tax in applied_taxes])) - partial_vals = { - 'name': name, - 'account_id': key['account_id'], - 'move_id': self.move_id.id, - 'tax_ids': [(6, 0, tax_ids)], - 'tax_tag_ids': [(6, 0, key['base_tag_ids'])], - 'product_id': product_id, - 'display_type': 'product', - 'product_uom_id': product_uom, - 'currency_id': self.currency_id.id, - 'amount_currency': sale_vals['amount'], - 'balance': sale_vals['amount_converted'], - 'quantity': sale_vals.get('quantity', 1.00) * key['sign'], - } - return partial_vals - - def _get_tax_vals(self, key, amount, amount_converted, base_amount_converted): - account_id, repartition_line_id, tag_ids = key - tax_rep = self.env['account.tax.repartition.line'].browse(repartition_line_id) - tax = tax_rep.tax_id - return { - 'name': tax.name, - 'account_id': account_id, - 'move_id': self.move_id.id, - 'tax_base_amount': abs(base_amount_converted), - 'tax_repartition_line_id': repartition_line_id, - 'tax_tag_ids': [(6, 0, tag_ids)], - 'display_type': 'tax', - 'currency_id': self.currency_id.id, - 'amount_currency': amount, - 'balance': amount_converted, - } - - def _get_combine_statement_line_vals(self, journal, amount, payment_method): - amount_values = self._prepare_statement_line_amount_values(journal, amount) - return { - 'date': fields.Date.context_today(self), - 'payment_ref': self.name, - 'pos_session_id': self.id, - 'journal_id': journal.id, - 'counterpart_account_id': self._get_receivable_account(payment_method).id, - **amount_values - } - - def _get_split_statement_line_vals(self, journal, amount, payment): - accounting_partner = self.env["res.partner"]._find_accounting_partner(payment.partner_id) - amount_values = self._prepare_statement_line_amount_values(journal, amount) - return { - 'date': fields.Date.context_today(self, timestamp=payment.payment_date), - 'payment_ref': payment.name, - 'pos_session_id': self.id, - 'journal_id': journal.id, - 'counterpart_account_id': accounting_partner.property_account_receivable_id.id, - 'partner_id': accounting_partner.id, - **amount_values - } - - def _prepare_statement_line_amount_values(self, journal, amount): - journal_currency = journal.currency_id or self.company_id.currency_id - if journal_currency == self.currency_id: - return {'amount': amount} - return { - 'amount': self.currency_id._convert(amount, journal_currency, self.company_id, self.stop_at), - 'amount_currency': amount, - 'foreign_currency_id': self.currency_id.id, - } - - def _update_amounts(self, old_amounts, amounts_to_add, date, round=True, force_company_currency=False): - """Responsible for adding `amounts_to_add` to `old_amounts` considering the currency of the session. - - old_amounts { new_amounts { - amount amounts_to_add { amount - amount_converted + amount -> amount_converted - [base_amount [base_amount] [base_amount - base_amount_converted] } base_amount_converted] - } } - - NOTE: - - Notice that `amounts_to_add` does not have `amount_converted` field. - This function is responsible in calculating the `amount_converted` from the - `amount` of `amounts_to_add` which is used to update the values of `old_amounts`. - - Values of `amount` and/or `base_amount` should always be in session's currency [1]. - - Value of `amount_converted` should be in company's currency - - [1] Except when `force_company_currency` = True. It means that values in `amounts_to_add` - is in company currency. - - :param dict old_amounts: - Amounts to update - :param dict amounts_to_add: - Amounts used to update the old_amounts - :param date date: - Date used for conversion - :param bool round: - Same as round parameter of `res.currency._convert`. - Defaults to True because that is the default of `res.currency._convert`. - We put it to False if we want to round globally. - :param bool force_company_currency: - If True, the values in amounts_to_add are in company's currency. - Defaults to False because it is only used to anglo-saxon lines. - - :returns: new amounts combining the values of `old_amounts` and `amounts_to_add`. - :rtype: dict - """ - # make a copy of the old amounts - new_amounts = { **old_amounts } - - amount = amounts_to_add.get('amount') - if self.is_in_company_currency or force_company_currency: - amount_converted = amount - else: - amount_converted = self._amount_converter(amount, date, round) - - # update amount and amount converted - new_amounts['amount'] += amount - new_amounts['amount_converted'] += amount_converted - - # consider base_amount if present - - if amounts_to_add.get('base_amount'): - base_amount = amounts_to_add.get('base_amount') - - # update base_amount and base_amount_converted - new_amounts['base_amount'] += base_amount - new_amounts['base_amount_converted'] += base_amount - - return new_amounts - - def _credit_amounts(self, partial_move_line_vals, amount, amount_converted, force_company_currency=False): - """ `partial_move_line_vals` is completed by `credit`ing the given amounts. - - NOTE Amounts in PoS are in the currency of journal_id in the session.config_id. - This means that amount fields in any pos record are actually equivalent to amount_currency - in account module. Understanding this basic is important in correctly assigning values for - 'amount' and 'amount_currency' in the account.move.line record. - - :param dict partial_move_line_vals: - initial values in creating account.move.line - :param float amount: - amount derived from pos.payment, pos.order, or pos.order.line records - :param float amount_converted: - converted value of `amount` from the given `session_currency` to company currency - - :return: complete values for creating 'amount.move.line' record - :rtype: dict - """ - if self.is_in_company_currency or force_company_currency: - additional_field = {} - else: - additional_field = { - 'amount_currency': -amount, - 'currency_id': self.currency_id.id, - } - return { - 'debit': -amount_converted if amount_converted < 0.0 else 0.0, - 'credit': amount_converted if amount_converted > 0.0 else 0.0, - **partial_move_line_vals, - **additional_field, - } - - def _debit_amounts(self, partial_move_line_vals, amount, amount_converted, force_company_currency=False): - """ `partial_move_line_vals` is completed by `debit`ing the given amounts. - - See _credit_amounts docs for more details. - """ - if self.is_in_company_currency or force_company_currency: - additional_field = {} - else: - additional_field = { - 'amount_currency': amount, - 'currency_id': self.currency_id.id, - } - return { - 'debit': amount_converted if amount_converted > 0.0 else 0.0, - 'credit': -amount_converted if amount_converted < 0.0 else 0.0, - **partial_move_line_vals, - **additional_field, - } + return self.config_id.default_partner_id.property_account_receivable_id def _amount_converter(self, amount, date, round): # self should be single record as this method is only called in the subfunctions of self._validate_session return self.currency_id._convert(amount, self.company_id.currency_id, self.company_id, date, round=round) - def show_cash_register(self): - return { - 'name': _('Cash register'), - 'type': 'ir.actions.act_window', - 'res_model': 'account.bank.statement.line', - 'view_mode': 'list,kanban', - 'domain': [('id', 'in', self.statement_line_ids.ids)], - } - def show_journal_items(self): self.ensure_one() all_related_moves = self._get_related_account_moves() @@ -1473,39 +594,27 @@ def show_journal_items(self): 'type': 'ir.actions.act_window', 'res_model': 'account.move.line', 'view_mode': 'list', - 'view_id':self.env.ref('account.view_move_line_tree').id, + 'view_id': self.env.ref('account.view_move_line_tree').id, 'domain': [('id', 'in', all_related_moves.mapped('line_ids').ids)], 'context': { - 'journal_type':'general', - 'search_default_group_by_move': 1, - 'group_by':'move_id', 'search_default_posted':1, + 'journal_type': 'general', + 'search_default_group_by_sales_move': 1, + 'group_by': 'move_id', 'search_default_posted': 1, }, } - def _get_other_related_moves(self): - # TODO This is not an ideal way to get the diff account.move's for - # the session. It would be better if there is a relation field where - # these moves are saved. - - # Unfortunately, the 'ref' of account.move is not indexed, so - # we are querying over the account.move.line because its 'ref' is indexed. - # And yes, we are only concern for split bank payment methods. - diff_lines_ref = [self._get_diff_account_move_ref(pm) for pm in self.payment_method_ids if pm.type == 'bank' and pm.split_transactions] - cost_move_lines = ['pos_order_'+str(rec.id) for rec in self._get_closed_orders()] - return self.env['account.move.line'].search([('ref', 'in', diff_lines_ref + cost_move_lines)]).mapped('move_id') - def _get_related_account_moves(self): invoices = self.mapped('order_ids.account_move') invoice_payments = self.mapped('order_ids.payment_ids.account_move_id') - cash_moves = self.statement_line_ids.mapped('move_id') - bank_payment_moves = self.bank_payment_ids.mapped('move_id') + cash_moves = self.bank_statement_line_ids.mapped('move_id') reversal_moves = self.mapped('order_ids.reversed_move_ids') - other_related_moves = self._get_other_related_moves() - return invoices | invoice_payments | self.move_id | cash_moves | bank_payment_moves | reversal_moves | other_related_moves - - def _get_receivable_account(self, payment_method): - """Returns the default pos receivable account if no receivable_account_id is set on the payment method.""" - return payment_method.receivable_account_id or self.company_id.account_default_pos_receivable_account_id + return invoices |\ + invoice_payments |\ + self.sales_move_id |\ + self.refunds_move_id |\ + self.correction_move_ids |\ + cash_moves |\ + reversal_moves def action_show_payments_list(self): return { @@ -1514,7 +623,7 @@ def action_show_payments_list(self): 'res_model': 'pos.payment', 'view_mode': 'list,form', 'domain': self._get_captured_payments_domain(), - 'context': {'search_default_group_by_payment_method': 1} + 'context': {'search_default_group_by_payment_method': 1}, } def _get_captured_payments_domain(self): @@ -1539,14 +648,13 @@ def _set_opening_control_data(self, cashbox_value: int, notes: str): """ self.state = 'opened' self.start_at = fields.Datetime.now() - cash_payment_method_ids = self.config_id.payment_method_ids.filtered(lambda pm: pm.is_cash_count) + cash_pm = self.config_id._get_cash_payment_method() + self._handle_cash_statement_entries({ + cash_pm.id: cashbox_value, + }) + if notes: self.opening_notes = notes - if cash_payment_method_ids: - difference = cashbox_value - self.cash_register_balance_start - self._post_cash_details_message('Opening cash', self.cash_register_balance_start, difference, notes) - self.cash_register_balance_start = cashbox_value - elif notes: message = _('Opening control message: ') message += notes self.message_post(body=plaintext2html(message)) @@ -1561,34 +669,20 @@ def set_opening_control(self, cashbox_value: int, notes: str): if self.state != 'opening_control': return + sequence_ctx = self.env['ir.sequence'].with_context( + company_id=self.config_id.company_id.id, + ) + sequence = sequence_ctx.search([ + ('code', '=', 'pos.session'), + ('company_id', 'in', [self.config_id.company_id.id, False]), + ], order='company_id', limit=1) + + first = (self.config_id.name if sequence.prefix == '/' else '') + second = sequence.next_by_code('pos.session') + third = (self.name if self.name != '/' else '') + self.name = first + second + third self._set_opening_control_data(cashbox_value, notes) - sequence = self.env['ir.sequence'].with_context( - company_id=self.config_id.company_id.id - ).search([('code', '=', 'pos.session'), ('company_id', 'in', [self.config_id.company_id.id, False])], order='company_id', limit=1) - - self.name = (self.config_id.name if sequence.prefix == '/' else '') + sequence.next_by_code('pos.session') + (self.name if self.name != '/' else '') - - def _post_cash_details_message(self, state, expected, difference, notes): - expected_formatted = self.currency_id.format(expected) - difference_formatted = self.currency_id.format(difference) - counted_formatted = self.currency_id.format(expected + difference) - - if state == 'Opening cash': - message = _("Opening cash difference: %s \n", difference_formatted) - message += _("Opening cash expected: %s \n", expected_formatted) - message += _("Opening cash counted: %s \n", counted_formatted) - else: - message = _("Closing difference: %s \n", difference_formatted) - message += _("Closing expected: %s \n", expected_formatted) - message += _("Closing counted: %s \n", counted_formatted) - - if notes: - message += _('Opening control message: ') - message += notes - if message: - self.message_post(body=plaintext2html(message), email_from=self.env.user.email or "admin@example.com") - def action_view_order(self): return { 'name': _('Orders'), @@ -1615,7 +709,7 @@ def _alert_old_session(self): note=_( "Your PoS Session is open since %(date)s, we advise you to close it and to create a new one.", date=session.start_at, - ) + ), ) def _check_if_no_draft_orders(self): @@ -1624,38 +718,26 @@ def _check_if_no_draft_orders(self): raise UserError(_( 'There are still orders in draft state in the session. ' 'Pay or cancel the following orders to validate the session:\n%s', - ', '.join(draft_orders.mapped('name')) + ', '.join(draft_orders.mapped('name')), )) return True - def _prepare_account_bank_statement_line_vals(self, session, sign, amount, reason, partner_id, extras): - return { - 'pos_session_id': session.id, - 'journal_id': session.cash_journal_id.id, - 'amount': sign * amount, - 'date': fields.Date.context_today(self), - 'payment_ref': '-'.join([session.name, extras['translatedType'], reason]), - 'partner_id': partner_id, - } - - def try_cash_in_out(self, _type, amount, reason, partner_id, extras): + def try_cash_in_out(self, _type, amount, reason, partner_id): sign = 1 if _type == 'in' else -1 - sessions = self.filtered('cash_journal_id') - if not sessions: + cash_pm = self.config_id._get_cash_payment_method() + if not cash_pm: raise UserError(_("There is no cash payment method for this PoS Session")) - vals_list = [ - self._prepare_account_bank_statement_line_vals(session, sign, amount, reason, partner_id, extras) - for session in sessions - ] - - self.env['account.bank.statement.line'].with_context(no_retrieve_partner=True).create(vals_list) + message = f'{self.name}-{_type}-{reason}' + signed_amount = amount * sign + partner = self.env['res.partner'].browse(partner_id) + cash_pm._create_cash_payment_line(self, signed_amount, partner, message) def delete_cash_in_out(self, absl_id, partner_id): if not self.env.user.has_group('account.group_account_basic'): raise AccessError(_("You don't have the access rights to delete a cash in/out.")) absl = self.env['account.bank.statement.line'].browse(absl_id) - if absl not in self.statement_line_ids: + if absl not in self.bank_statement_line_ids: raise AccessError(_("You cannot delete a cash move that is not linked to this session.")) cashier_name = absl.partner_id.name amount = absl.amount @@ -1665,7 +747,7 @@ def delete_cash_in_out(self, absl_id, partner_id): def _get_invoice_total_list(self): invoice_list = [] - for order in self.order_ids.filtered(lambda o: o.is_invoiced): + for order in self.order_ids.filtered(lambda o: o.is_singly_invoiced): invoice = { 'total': order.account_move.amount_total_signed, 'name': order.account_move.name, @@ -1677,9 +759,8 @@ def _get_invoice_total_list(self): def _get_total_invoice(self): amount = 0 - for order in self.order_ids.filtered(lambda o: o.is_invoiced): + for order in self.order_ids.filtered(lambda o: o.is_singly_invoiced): amount += order.amount_paid - return amount def log_partner_message(self, partner_id, action, message_type): @@ -1689,8 +770,426 @@ def log_partner_message(self, partner_id, action, message_type): body = _('Cash drawer opened (%(ACTION)s)', ACTION=action) elif message_type == 'CASH_IN_OUT_UNLINK': body = _('Cash move deleted: %s', action) - self.message_post(body=body, author_id=partner_id) def _get_closed_orders(self): return self.order_ids.filtered(lambda o: o.state not in ['draft', 'cancel']) + + ############################################################## + # Accounting related methods # + ############################################################## + def _handle_bank_payment_method_difference(self, payment_method_closing={}): + """ + This method will create a new account.move after session closing + for each bank payment method with a difference between the amount of + the payments and the amount counted in the closing. This can happen + when the cashier forget to enter an amount for a bank payment method + in the closing, or when there is a difference between the amount entered + and the amount of the payments + """ + other_payment_methods = self.payment_method_ids.filtered_domain([ + ('type', '=', 'bank'), + ]) + + for pm in other_payment_methods: + payments = self.order_ids.mapped('payment_ids').filtered( + lambda p: p.payment_method_id == pm, + ) + + # If not provided skip the reconciliation of the payment method, + # this can happen when the session is closed from the UI and not + # all the payment methods are sent by the frontend + if str(pm.id) not in payment_method_closing and pm.id not in payment_method_closing: + continue + + counted = payment_method_closing.get(str(pm.id), 0) + counted = counted or payment_method_closing.get(pm.id, 0) + diff = sum(payments.mapped('amount')) - counted + + if float_is_zero(diff, precision_rounding=self.currency_id.rounding): + continue + + journal = pm.journal_id + if not journal: + continue + + pm_account = pm.receivable_account_id or self._get_receivable_account() + correction_account = journal.loss_account_id if diff > 0 else journal.profit_account_id + + if not correction_account: + continue + + abs_difference = abs(diff) + move_ctx = self.env['account.move'].sudo().with_context( + linked_to_pos=True, + ) + move = move_ctx.create({ + 'journal_id': journal.id, + 'date': fields.Date.context_today(self), + 'ref': _( + 'Bank difference for %(pm)s in %(session)s', + pm=pm.name, + session=self.name, + ), + 'line_ids': [ + Command.create({ + 'name': pm.name, + 'account_id': pm_account.id, + 'amount_currency': abs_difference if diff < 0 else -abs_difference, + }), + Command.create({ + 'name': pm.name, + 'account_id': correction_account.id, + 'amount_currency': -abs_difference if diff < 0 else abs_difference, + }), + ], + }) + move._post() + self.correction_move_ids |= move + + def _handle_cash_statement_entries(self, payment_method_closing={}): + """ + Called at the opening and closing of the session, this method + will create the necessary account.bank.statement and account.bank.statement.line + records to reflect the cash movements of the session in the cash + statement linked to the session. + """ + cash_pm = self.config_id._get_cash_payment_method() + if not cash_pm: + return False + + counted = payment_method_closing.get(str(cash_pm.id), 0) + counted = counted or payment_method_closing.get(cash_pm.id, 0) + if not self.bank_statement_id: + last_balance = self.config_id._get_opening_balance() + self.bank_statement_id = self.env['account.bank.statement'].sudo().create({ + 'journal_id': cash_pm.journal_id.id, + 'balance_start': last_balance, + 'name': _( + 'Cash Statement for %(method_name)s in %(session)s', + method_name=cash_pm.name, + session=self.name, + ), + }) + difference = counted - last_balance + else: + end = self.bank_statement_id.balance_end + difference = counted - end + + rounding = self.currency_id.rounding + if not float_is_zero(difference, precision_rounding=rounding): + message = _( + 'Cash correction from %(session)s', + session=self.name, + ) + partner = self.env.user.partner_id + cash_pm._create_cash_payment_line( + self, + difference, + partner, + message, + ) + + return True + + def _get_receivable_account(self): + """ + PoS session receivable account is now accessed through the linked + default partner of the linked config. + """ + self.config_id.ensure_one() + return self.config_id.default_partner_id.property_account_receivable_id + + def _validate_session_accounting(self, check_validity=True): + """ + This method is the ONLY entry point for the session closing + process, and should contain all the necessary logic to create + the accounting entries of the session closing. + """ + self.ensure_one() + + # Get all paid and invoiced orders of the session + non_invoiced_orders, invoiced_orders = self._get_invoiced_and_non_invoiced_orders() + self._check_invoiced_orders_are_posted(invoiced_orders) + + # Build the out_receipt lines. Returns pm_data_list so we can + # create the matching account.payment / statement line records after posting. + sale_orders = non_invoiced_orders.filtered( + lambda order: not order.is_refund and order.amount_total > 0, + ) + refund_orders = non_invoiced_orders - sale_orders + sales_move = self._create_session_account_move(sale_orders, check_validity) + refunds_move = self._create_session_account_move(refund_orders, check_validity) + self.sales_move_id = sales_move + self.refunds_move_id = refunds_move + + # Ensure tracking of pos orders in the account moves + sale_orders.account_move = sales_move + refund_orders.account_move = refunds_move + return abs(sales_move.amount_residual) + abs(refunds_move.amount_residual) + + def _prepare_session_closing_extra_line_commands(self, orders, refund, payments=[]): + """ Inherited in pos_stock """ + return [] + + def _prepare_session_move_vals(self, orders): + self.ensure_one() + today = fields.Date.context_today(self) + + move_type = 'out_refund' if orders[0].is_refund else 'out_invoice' + + return { + 'move_type': move_type, + 'company_id': self.company_id.id, + 'journal_id': self.config_id.journal_id.id, + 'ref': _("Closing entry for session %s", self.name), + 'partner_id': self.config_id.default_partner_id.id, + 'date': today, + 'invoice_date_due': today, + } + + def _create_session_account_move(self, orders, check_validity=True): + """ + This method creates the receipt of the session closing, with all + the details of the session accounting. This will only take into + account the orders that were paid but not invoiced, as the ones + that were invoiced already have their details in the invoice. + + We'll create following account.move.line: + - One line per (revenue account + VAT rate) group with net amount + tax_ids + - One tax line per (tax account + tax) combination + - One line per payment method with the total amount + (display_type='payment_term' on the POS receivable account, + so it can be reconciled with account.payment) + + After posting, one account.payment is created per payment method + and reconciled against the matching payment_term line, marking + the receipt as fully paid via standard Odoo reconciliation. + + Returns the pm_data_list (list of dicts) for payment creation + in _validate_session_accounting. + """ + if not orders: + return self.env['account.move'] + + refund = orders[0].is_refund # All orders are refunds or not + AccountJournal = self.env['account.journal'].with_company( + self.company_id, + ) + journal = AccountJournal._ensure_company_account_journal() + config_journal = self.config_id.journal_id + if self.config_id.journal_id != journal and config_journal.type != 'sale': + self.config_id.journal_id = journal + + payment_methods = orders.payment_ids.payment_method_id + cash_payment_method = payment_methods.filtered( + lambda pm: pm.type == 'cash', + ) + + if len(cash_payment_method) > 1: + raise UserError(_( + "Only one cash payment method can be used in a session.", + )) + + # product_commands => invoice_line_ids (display_type=product, net price_unit) + lines = orders._prepare_account_move_line_data() + lines_commands = [Command.create(line['account.move.line']) for line in lines] + + payments = orders._prepare_account_move_line_data_for_payments() + line_data = [pm['account.move.line'] for pm in payments] + payment_commands = [Command.create(pm_data) for pm_data in line_data] + extra_commands = self._prepare_session_closing_extra_line_commands( + orders, + refund, + payments, + ) + + # Ensure rounding method record is set on the invoice if needed + rounding_method = self.env['account.cash.rounding'] + only_cash = self.config_id.only_round_cash_method + available_type = ['cash'] if only_cash else ['cash', 'bank'] + use_rounding = orders.payment_ids.filtered_domain([ + ('payment_method_id.type', 'in', available_type), + ('pos_order_id.amount_difference', '!=', 0), + ]) + if self.config_id.cash_rounding and use_rounding: + rounding_method = self.config_id.rounding_method + + move_vals = self._prepare_session_move_vals(orders) + move_vals.update({ + 'invoice_line_ids': lines_commands, + 'line_ids': payment_commands, + 'invoice_cash_rounding_id': rounding_method.id, + }) + move = self.env['account.move'].sudo().with_context( + check_move_validity=False, + always_tax_exigible=True, + linked_to_pos=True, + ).create(move_vals) + + move_ctx = move.with_context( + linked_to_pos=True, + skip_invoice_sync=True, + check_move_validity=check_validity, + ) + + if len(extra_commands) > 0: + move_ctx.with_context( + check_move_validity=False, + ).write({'line_ids': extra_commands}) + + # Ensure account_id is always the good one, sometime due to the + # compute method on account_id in the account.move.line model, + # the account_id on payment_commands is not the one expected, + # so we set it again here to be sure. + payment_term_lines = move.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + zipped = zip(payment_commands, payment_term_lines) + for payment_command, term_line in zipped: + term_line.account_id = payment_command[2]['account_id'] + + with move_ctx._check_balanced({'records': move}): + if use_rounding: + data = orders._prepare_account_move_line_data_for_rounding(move) + move_ctx.line_ids = data + + # A rounded foreign-currency payment converts to a slightly different company-currency + # total than the sum of individually converted product/tax balances. Absorb the diff. + summary = sum(move.line_ids.mapped('balance')) + balance_diff = self.company_id.currency_id.round(summary) + if move.currency_id != self.company_id.currency_id and balance_diff: + if balance_diff: + payment_term_lines = move.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + if payment_term_lines: + payment_term_lines[0].balance -= balance_diff + + move_ctx.with_company(self.company_id)._post() + payment_lines = self.env['pos.order']._create_payment_moves( + self, + payments, + ) + + payment_lines = payment_lines.filtered( + lambda line: not line.reconciled, + ) + payment_term_lines = payment_term_lines.filtered( + lambda line: not line.reconciled, + ) + + # We cannot reconcile automatically all lines together because + # sometime it create weird reconciliation with multiple payments + for idx, term in enumerate(payment_term_lines): + payment_line = payment_lines[idx] + (payment_line + term).with_context(skip_invoice_sync=True).reconcile() + + return move + + def _get_invoiced_and_non_invoiced_orders(self): + """ Return the paid orders of the session that are not invoiced. """ + self.ensure_one() + orders = self._get_closed_orders() + invoiced_orders = orders.filtered(lambda o: o.is_singly_invoiced) + non_invoiced_orders = orders - invoiced_orders + return non_invoiced_orders, invoiced_orders + + def _check_invoiced_orders_are_posted(self, invoiced_orders): + account_move = invoiced_orders.account_move + unposted = account_move.filtered(lambda move: move.state != 'posted') + if unposted: + invoices = '\n'.join(f'{invoice.name} - {invoice.state}' for invoice in unposted) + raise UserError(_( + 'You cannot close the POS when invoices are not posted.\nInvoices: %(invoices)s', + invoices=invoices, + )) + + def _prepare_account_move_line_commands_for_reversal(self, order, invoice_to_reverse): + product_lines = invoice_to_reverse.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + reverse_move_lines = [] + for line in product_lines: + reverse_move_lines.append(Command.create({ + 'name': _("Partial reversal of %s", line.name), + 'product_id': line.product_id.id, + 'account_id': line.account_id.id, + 'partner_id': line.partner_id.id, + 'currency_id': order.company_id.currency_id.id, + 'amount_currency': -line.amount_currency, + 'balance': -line.amount_currency, + 'display_type': line.display_type, + 'tax_ids': [(6, 0, line.tax_ids.ids)], + 'quantity': -line.quantity, + })) + return reverse_move_lines + + def _create_partial_reversal_move_from_session_closing(self, order): + """ + Create a misc move to reverse POS orders and "remove" it from the + POS closing entry. This is done by taking data from the orders + and using it to somewhat replicate the resulting entry in orders + to reverse partially the movements done in the POS closing entry. + """ + self.ensure_one() + order.ensure_one() + order.account_move.ensure_one() + + reverse_move_lines = [] + invoice_to_reverse = order.account_move + original_move = self.refunds_move_id if order.is_refund else self.sales_move_id + reverse_move_lines += self._prepare_account_move_line_commands_for_reversal( + order, + invoice_to_reverse, + ) + + rounding_line = invoice_to_reverse.line_ids.filtered( + lambda line: line.display_type == 'rounding', + ) + if rounding_line: + matching_line = original_move.line_ids.filtered( + lambda line: line.display_type == 'rounding', + ) + reverse_move_lines.append(Command.create({ + 'name': _("Partial reversal of rounding: %s", matching_line.name), + 'account_id': matching_line.account_id.id, + 'partner_id': matching_line.partner_id.id, + 'currency_id': order.company_id.currency_id.id, + 'amount_currency': -rounding_line.amount_currency, + 'balance': -rounding_line.balance, + 'display_type': matching_line.display_type, + })) + + payment_lines = invoice_to_reverse.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + for idx, payment in enumerate(payment_lines): + matching_line = original_move.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + )[idx] + receivable_line = Command.create({ + 'name': _("Reversal of payment: %s", matching_line.name), + 'account_id': matching_line.account_id.id, + 'partner_id': matching_line.partner_id.id, + 'currency_id': order.company_id.currency_id.id, + 'amount_currency': -payment.amount_currency, + 'balance': -payment.balance, + 'display_type': matching_line.display_type, + }) + reverse_move_lines.append(receivable_line) + + Move = self.env['account.move'].sudo().with_company(order.company_id) + move_ctx = Move.with_context( + linked_to_pos=True, + ) + + return move_ctx.create({ + 'invoice_cash_rounding_id': invoice_to_reverse.invoice_cash_rounding_id.id, + 'date': fields.Date.today(), + 'reversed_pos_order_id': order.id, + 'ref': _("Partial reversal of invoice %s", original_move.name), + 'line_ids': reverse_move_lines, + 'journal_id': original_move.journal_id.id, + 'reversed_entry_id': original_move.id, + }) diff --git a/addons/point_of_sale/models/report_sale_details.py b/addons/point_of_sale/models/report_sale_details.py index 3bc2c77e3c4634..e3d663368e2a9b 100644 --- a/addons/point_of_sale/models/report_sale_details.py +++ b/addons/point_of_sale/models/report_sale_details.py @@ -112,13 +112,13 @@ def get_sale_details(self, date_start=False, date_stop=False, config_ids=False, if payment_ids: method_name = self.env['pos.payment.method']._field_to_sql('method', 'name') self.env.cr.execute(SQL(""" - SELECT method.id as id, payment.session_id as session, %(method_name)s as name, method.is_cash_count as cash, + SELECT method.id as id, payment.session_id as session, %(method_name)s as name, method.type = 'cash' as cash, sum(amount) total, method.journal_id journal_id FROM pos_payment AS payment, pos_payment_method AS method WHERE payment.payment_method_id = method.id AND payment.id IN %(payment_ids)s - GROUP BY method.name, method.is_cash_count, payment.session_id, method.id, journal_id + GROUP BY method.name, cash, payment.session_id, method.id, journal_id ORDER BY method.id, payment.session_id """, method_name=method_name, payment_ids=tuple(payment_ids))) payments = self.env.cr.dictfetchall() @@ -155,15 +155,14 @@ def get_sale_details(self, date_start=False, date_stop=False, config_ids=False, for session in sessions: cash_counted = 0 - if session.cash_register_balance_end_real: - cash_counted = session.cash_register_balance_end_real + if session.closing_balance: + cash_counted = session.closing_balance is_cash_method = False for payment in payments: account_payments = self.env['account.payment'].search([('pos_session_id', '=', session.id)]) if payment['session'] == session.id: if not payment['cash']: - ref_value = "Closing difference in %s (%s)" % (payment['name'], session.name) - account_move = self.env['account.move'].search([("ref", "=", ref_value)], limit=1) + account_move = session.correction_move_ids if account_move: payment_method = self.env['pos.payment.method'].browse(payment['id']) is_loss = any(l.account_id == payment_method.journal_id.loss_account_id for l in account_move.line_ids) @@ -194,17 +193,17 @@ def get_sale_details(self, date_start=False, date_stop=False, config_ids=False, payment['count'] = True else: is_cash_method = True - payment['final_count'] = payment['total'] + session.cash_register_balance_start + session.cash_real_transaction + payment['final_count'] = payment['total'] + session.opening_balance payment['money_counted'] = cash_counted payment['money_difference'] = payment['money_counted'] - payment['final_count'] cash_moves = self.env['account.bank.statement.line'].search([('pos_session_id', '=', session.id)]) cash_in_out_list = [] cash_in_count = 0 cash_out_count = 0 - if session.cash_register_balance_start > 0: + if session.opening_balance > 0: cash_in_out_list.append({ 'name': _('Cash Opening'), - 'amount': session.cash_register_balance_start, + 'amount': session.opening_balance, }) for cash_move in cash_moves: if cash_move.amount > 0: @@ -223,15 +222,15 @@ def get_sale_details(self, date_start=False, date_stop=False, config_ids=False, if not is_cash_method: cash_name = _('Cash %(session_name)s', session_name=session.name) previous_session = self.env['pos.session'].search([('id', '<', session.id), ('state', '=', 'closed'), ('config_id', '=', session.config_id.id)], limit=1) - final_count = previous_session.cash_register_balance_end_real + session.cash_real_transaction - cash_difference = session.cash_register_balance_end_real - final_count + final_count = previous_session.closing_balance + cash_difference = session.closing_balance - final_count cash_moves = self.env['account.bank.statement.line'].search([('pos_session_id', '=', session.id)], order='date asc') cash_in_out_list = [] - if previous_session.cash_register_balance_end_real > 0: + if previous_session.closing_balance > 0: cash_in_out_list.append({ 'name': _('Cash Opening'), - 'amount': previous_session.cash_register_balance_end_real, + 'amount': previous_session.closing_balance, }) # If there is a cash difference, we remove the last cash move which is the cash difference @@ -247,7 +246,7 @@ def get_sale_details(self, date_start=False, date_stop=False, config_ids=False, 'name': cash_name, 'total': 0, 'final_count': final_count, - 'money_counted': session.cash_register_balance_end_real, + 'money_counted': session.closing_balance, 'money_difference': cash_difference, 'cash_moves': cash_in_out_list, 'count': True, diff --git a/addons/point_of_sale/models/res_config_settings.py b/addons/point_of_sale/models/res_config_settings.py index 80ca054e851ba9..5124da181c716c 100644 --- a/addons/point_of_sale/models/res_config_settings.py +++ b/addons/point_of_sale/models/res_config_settings.py @@ -1,7 +1,6 @@ import logging from odoo import api, fields, models -from odoo.addons.point_of_sale.models.pos_config import format_epson_certified_domain _logger = logging.getLogger(__name__) @@ -39,7 +38,6 @@ def _default_pos_config(self): module_pos_qfpay = fields.Boolean(string="QFPay Payment Terminal", help="The transactions are processed by QFPay. Set your QFPay credentials on the related payment method.") module_pos_dpopay = fields.Boolean(string="DPO Pay Payment Terminal", help="The transactions are processed by DPO Pay. Set your DPO Pay credentials on the related payment method.") module_pos_pricer = fields.Boolean(string="Pricer electronic price tags", help="Display the price of your products through electronic price tags") - account_default_pos_receivable_account_id = fields.Many2one(string='Default Account Receivable (PoS)', related='company_id.account_default_pos_receivable_account_id', readonly=False, check_company=True) barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', related='company_id.nomenclature_id', readonly=False) use_kiosk_mode = fields.Boolean(string="Is Kiosk Mode", default=False) pos_customer_display_bg_img = fields.Image(related='pos_config_id.customer_display_bg_img', readonly=False) @@ -67,6 +65,7 @@ def _default_pos_config(self): custom_email_placeholder = fields.Char(related='pos_config_id.company_id.email', readonly=False) pos_custom_website = fields.Char(related='pos_config_id.custom_website', readonly=False) custom_website_placeholder = fields.Char(related='pos_config_id.company_id.website', readonly=False) + pos_default_partner_id = fields.Many2one(related='pos_config_id.default_partner_id', readonly=False) pos_allowed_pricelist_ids = fields.Many2many('product.pricelist', compute='_compute_pos_allowed_pricelist_ids') pos_amount_authorized_diff = fields.Float(related='pos_config_id.amount_authorized_diff', readonly=False) @@ -85,7 +84,6 @@ def _default_pos_config(self): pos_iface_print_skip_screen = fields.Boolean(related='pos_config_id.iface_print_skip_screen', readonly=False) pos_iface_tax_included = fields.Selection(related='pos_config_id.iface_tax_included', readonly=False) pos_iface_tipproduct = fields.Boolean(related='pos_config_id.iface_tipproduct', readonly=False) - pos_invoice_journal_id = fields.Many2one(related='pos_config_id.invoice_journal_id', readonly=False) pos_use_header_or_footer = fields.Boolean(related='pos_config_id.use_header_or_footer', readonly=False) pos_is_margins_costs_accessible_to_every_user = fields.Boolean(related='pos_config_id.is_margins_costs_accessible_to_every_user', readonly=False) pos_journal_id = fields.Many2one(related='pos_config_id.journal_id', readonly=False) diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv index 936b1b3b2505e7..5b5b1218e34046 100644 --- a/addons/point_of_sale/security/ir.model.access.csv +++ b/addons/point_of_sale/security/ir.model.access.csv @@ -7,9 +7,11 @@ access_report_pos_order,report.pos.order,model_report_pos_order,group_pos_user,1 access_account_journal_pos_user,account.journal pos_user,account.model_account_journal,group_pos_user,1,0,0,0 access_account_payment_method_pos_user,account.payment.method pos_user,account.model_account_payment_method,group_pos_manager,1,0,0,0 access_account_payment_method_line_pos_user,account.payment.method.line pos_user,account.model_account_payment_method_line,group_pos_manager,1,0,0,0 +access_account_bank_statement,account.bank.statement,model_account_bank_statement,point_of_sale.group_pos_user,1,1,1,0 access_account_bank_statement_line,account.bank.statement.line,account.model_account_bank_statement_line,group_pos_user,1,1,1,0 access_product_product,product.product,product.model_product_product,group_pos_user,1,0,0,0 access_product_template_pos_user,product.template pos user,product.model_product_template,group_pos_user,1,0,0,0 +access_account_bank_statement_manager,account.bank.statement,model_account_bank_statement,point_of_sale.group_pos_manager,1,1,1,1 access_account_bank_statement_line_manager,account.bank.statement.line manager,account.model_account_bank_statement_line,group_pos_manager,1,1,1,1 access_product_product_supplierinfo_user,product.supplierinfo user,product.model_product_supplierinfo,group_pos_user,1,0,0,0 access_product_pricelist_manager,product.pricelist manager,product.model_product_pricelist,group_pos_manager,1,1,1,1 diff --git a/addons/point_of_sale/security/point_of_sale_security.xml b/addons/point_of_sale/security/point_of_sale_security.xml index a098035ba83bb0..14ee3355a90f60 100644 --- a/addons/point_of_sale/security/point_of_sale_security.xml +++ b/addons/point_of_sale/security/point_of_sale_security.xml @@ -24,25 +24,6 @@
    - - - Point Of Sale Bank Statement Accountant - - - [(1, '=', 1)] - - - Point Of Sale Bank Statement Line POS User - - - [('pos_session_id', '!=', False)] - - - Point Of Sale Bank Statement Line Accountant - - - [(1, '=', 1)] - Point Of Sale Order diff --git a/addons/point_of_sale/static/src/app/components/popups/cash_move_popup/cash_move_popup.js b/addons/point_of_sale/static/src/app/components/popups/cash_move_popup/cash_move_popup.js index b331d67a32fffd..6cafe2191b3bbd 100644 --- a/addons/point_of_sale/static/src/app/components/popups/cash_move_popup/cash_move_popup.js +++ b/addons/point_of_sale/static/src/app/components/popups/cash_move_popup/cash_move_popup.js @@ -62,13 +62,12 @@ export class CashMovePopup extends Component { const type = this.state.type; const translatedType = _t(type); - const extras = { formattedAmount, translatedType }; const reason = this.state.reason.trim(); await this.pos.data.call( "pos.session", "try_cash_in_out", - this._prepareTryCashInOutPayload(type, amount, reason, this.partnerId, extras), + this._prepareTryCashInOutPayload(type, amount, reason, this.partnerId), {}, true ); @@ -109,8 +108,8 @@ export class CashMovePopup extends Component { ? this.env.utils.formatCurrency(parseFloat(value)) : ""; } - _prepareTryCashInOutPayload(type, amount, reason, partnerId, extras) { - return [[this.pos.session.id], type, amount, reason, partnerId, extras]; + _prepareTryCashInOutPayload(type, amount, reason, partnerId) { + return [[this.pos.session.id], type, amount, reason, partnerId]; } isValidCashMove() { return this.env.utils.isValidFloat(this.state.amount) && this.state.reason.trim() !== ""; diff --git a/addons/point_of_sale/static/src/app/components/popups/closing_popup/closing_popup.js b/addons/point_of_sale/static/src/app/components/popups/closing_popup/closing_popup.js index 063966cea1c2a2..6240fcbee7332a 100644 --- a/addons/point_of_sale/static/src/app/components/popups/closing_popup/closing_popup.js +++ b/addons/point_of_sale/static/src/app/components/popups/closing_popup/closing_popup.js @@ -5,12 +5,11 @@ import { ConfirmationDialog, AlertDialog } from "@web/core/confirmation_dialog/c import { MoneyDetailsPopup } from "@point_of_sale/app/components/popups/money_details_popup/money_details_popup"; import { useService } from "@web/core/utils/hooks"; import { Component } from "@odoo/owl"; -import { ConnectionLostError } from "@web/core/network/rpc"; import { _t } from "@web/core/l10n/translation"; import { usePos } from "@point_of_sale/app/hooks/pos_hook"; import { parseFloat } from "@web/views/fields/parsers"; import { useAsyncLockedMethod } from "@point_of_sale/app/hooks/hooks"; -import { ask } from "@point_of_sale/app/utils/make_awaitable_dialog"; +import { ask, makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog"; import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog"; import { PaymentMethodBreakdown } from "@point_of_sale/app/components/payment_method_breakdown/payment_method_breakdown"; import { CashInput } from "@point_of_sale/app/components/inputs/input/cash_input/cash_input"; @@ -193,128 +192,84 @@ export class ClosePosPopup extends Component { if (!syncSuccess) { return; } - if (this.pos.config.cash_control) { - const response = await this.pos.data.call( - "pos.session", - "post_closing_cash_details", - [this.pos.session.id], - { - counted_cash: parseFloat( - this.state.payments[this.props.default_cash_details.id].counted - ), - } - ); - - if (!response.successful) { - return this.handleClosingError(response); - } - } - - try { - await this.pos.data.call("pos.session", "update_closing_control_state_session", [ - this.pos.session.id, - this.state.notes, - ]); - } catch (error) { - // We have to handle the error manually otherwise the validation check stops the script. - // In case of "rescue session", we want to display the next popup with "handleClosingError". - // FIXME - if (!error.data && error.data.message !== "This session is already closed.") { - throw error; - } - } try { - const bankPaymentMethodDiffPairs = this.props.non_cash_payment_methods - .filter((pm) => pm.type == "bank") - .map((pm) => [pm.id, this.getDifference(pm.id)]); + const context = { + device_identifier: this.pos.device.identifier, + }; + const amountByPaymentMethod = Object.entries(this.state.payments).reduce( + (acc, [id, { counted }]) => { + acc[id] = parseFloat(counted); + return acc; + }, + {} + ); const response = await this.pos.data.call( "pos.session", "close_session_from_ui", - [this.pos.session.id, bankPaymentMethodDiffPairs], - { - context: { - device_identifier: this.pos.device.identifier, - }, - } + [this.pos.session.id, amountByPaymentMethod], + { context } ); - if (!response.successful) { + if (!response.status) { return this.handleClosingError(response); } + this.pos.session.state = "closed"; this.pos.router.close(); - } catch (error) { - if (error instanceof ConnectionLostError) { - throw error; - } else { - await this.handleClosingControlError(); - } } finally { localStorage.removeItem(`pos.session.${odoo.pos_config_id}`); } } - async handleClosingControlError() { - this.dialog.add( - AlertDialog, - { - title: _t("Closing session error"), - body: _t( - "An error has occurred when trying to close the session.\n" + - "You will be redirected to the back-end to manually close the session." - ), - }, - { - onClose: () => { - this.dialog.add( - FormViewDialog, - { - resModel: "pos.session", - resId: this.pos.session.id, - }, - { - onClose: async () => { - const session = await this.pos.data.read("pos.session", [ - this.pos.session.id, - ]); - if (session[0] && session[0].state === "closed") { - this.pos.router.close(); - } else { - this.pos.redirectToBackend(); - } - }, - } - ); - }, - } - ); - } async handleClosingError(response) { - this.dialog.add(ConfirmationDialog, { - title: response.title || "Oh snap !", - body: response.message, - confirmLabel: _t("Review Orders"), - cancelLabel: _t("Cancel Orders"), - confirm: () => { - if (!response.redirect) { - this.props.close(); - this.pos.navigate("TicketScreen"); - } - }, - cancel: async () => { - if (!response.redirect) { - const now = DateTime.now(); - const ordersDraft = this.pos.models["pos.order"].filter( - (o) => !o.finalized && !(o.preset_time && o.preset_time > now) - ); - await this.pos.deleteOrders(ordersDraft, response.open_order_ids); - this.closeSession(); + if (response.type === "accounting_error") { + return await makeAwaitable( + this.dialog, + AlertDialog, + { + title: _t("An error occurred while closing the session"), + body: response.message, + }, + { + onClose: () => { + this.dialog.add( + FormViewDialog, + { + resModel: "pos.session", + resId: this.pos.session.id, + }, + { + onClose: async () => { + this.pos.redirectToBackend(); + }, + } + ); + }, } - }, - dismiss: async () => {}, - }); - - if (response.redirect) { - this.pos.router.close(); + ); + } else if (response.type === "draft_orders") { + return await makeAwaitable(this.dialog, ConfirmationDialog, { + title: response.title || "Oh snap !", + body: response.message, + confirmLabel: _t("Review Orders"), + cancelLabel: _t("Cancel Orders"), + confirm: () => { + if (!response.redirect) { + this.props.close(); + this.pos.navigate("TicketScreen"); + } + }, + cancel: async () => { + if (!response.redirect) { + const now = DateTime.now(); + const ordersDraft = this.pos.models["pos.order"].filter( + (o) => !o.finalized && !(o.preset_time && o.preset_time > now) + ); + await this.pos.deleteOrders(ordersDraft, response.open_order_ids); + this.closeSession(); + } + }, + dismiss: async () => {}, + }); } } getMovesTotalAmount() { diff --git a/addons/point_of_sale/static/src/app/components/popups/opening_control_popup/opening_control_popup.js b/addons/point_of_sale/static/src/app/components/popups/opening_control_popup/opening_control_popup.js index f43bf4c1173cf5..a9d3c782644968 100644 --- a/addons/point_of_sale/static/src/app/components/popups/opening_control_popup/opening_control_popup.js +++ b/addons/point_of_sale/static/src/app/components/popups/opening_control_popup/opening_control_popup.js @@ -27,7 +27,7 @@ export class OpeningControlPopup extends Component { this.state = useState({ notes: "", openingCash: this.env.utils.formatCurrency( - this.pos.session.cash_register_balance_start || 0, + this.pos.config._last_opening_balance || 0, false ), }); @@ -88,6 +88,6 @@ export class OpeningControlPopup extends Component { this.state.openingCash = this.env.utils.parseAndFormatCurrency(this.state.openingCash); } get cashMethodCount() { - return this.pos.config.payment_method_ids.filter((pm) => pm.is_cash_count).length; + return this.pos.config.payment_method_ids.filter((pm) => pm.type === "cash").length; } } diff --git a/addons/point_of_sale/static/src/app/components/popups/send_receipt_popup/send_receipt_popup.xml b/addons/point_of_sale/static/src/app/components/popups/send_receipt_popup/send_receipt_popup.xml index 75577dba515c00..0ad3a9f8a367d5 100644 --- a/addons/point_of_sale/static/src/app/components/popups/send_receipt_popup/send_receipt_popup.xml +++ b/addons/point_of_sale/static/src/app/components/popups/send_receipt_popup/send_receipt_popup.xml @@ -17,7 +17,7 @@
    Sending in progress
    - Receipt and invoice sent successfully + Receipt and invoice sent successfully Receipt sent successfully
    Sending failed. Please try again
    diff --git a/addons/point_of_sale/static/src/app/models/accounting/pos_order_accounting.js b/addons/point_of_sale/static/src/app/models/accounting/pos_order_accounting.js index 2e138fb60c94a5..84d9fb27a1af18 100644 --- a/addons/point_of_sale/static/src/app/models/accounting/pos_order_accounting.js +++ b/addons/point_of_sale/static/src/app/models/accounting/pos_order_accounting.js @@ -119,7 +119,7 @@ export class PosOrderAccounting extends Base { return this.config.cash_rounding; } get orderIsRounded() { - const cashPm = this.payment_ids.some((p) => p.payment_method_id.is_cash_count); + const cashPm = this.payment_ids.some((p) => p.payment_method_id.type === "cash"); return this.config.hasGlobalRounding || (cashPm && this.config.hasCashRounding); } get appliedRounding() { @@ -201,7 +201,7 @@ export class PosOrderAccounting extends Base { * The whole order is rounded instead. */ shouldRound(paymentMethod) { - return paymentMethod.is_cash_count && this.config.hasCashRounding; + return paymentMethod.type === "cash" && this.config.hasCashRounding; } /** @@ -303,7 +303,7 @@ export class PosOrderAccounting extends Base { // Cash rounding is added only if the document needs to be globaly rounded. // See cash_rounding and only_round_cash_method config fields. - const cashRounding = this.config.cash_rounding ? this.config.rounding_method : null; + const cashRounding = this.config.hasGlobalRounding ? this.config.rounding_method : null; const data = accountTaxHelpers.get_tax_totals_summary(baseLines, currency, company, { cash_rounding: cashRounding, }); diff --git a/addons/point_of_sale/static/src/app/models/accounting/pos_order_line_accounting.js b/addons/point_of_sale/static/src/app/models/accounting/pos_order_line_accounting.js index c518febd55723c..77c9d7058fd263 100644 --- a/addons/point_of_sale/static/src/app/models/accounting/pos_order_line_accounting.js +++ b/addons/point_of_sale/static/src/app/models/accounting/pos_order_line_accounting.js @@ -183,7 +183,7 @@ export class PosOrderlineAccounting extends Base { product_uom_id: product.uom_id, product_id: product, rate: 1.0, - is_refund: this.qty * priceUnit < 0, + is_refund: this.order_id.is_refund, ...customValues, }; if (order?.fiscal_position_id && product !== this.config.discount_product_id) { diff --git a/addons/point_of_sale/static/src/app/models/pos_config.js b/addons/point_of_sale/static/src/app/models/pos_config.js index e2f4a1df723b20..b50e566d69a10c 100644 --- a/addons/point_of_sale/static/src/app/models/pos_config.js +++ b/addons/point_of_sale/static/src/app/models/pos_config.js @@ -66,7 +66,7 @@ export class PosConfig extends Base { return this.cash_rounding && !this.only_round_cash_method; } get canInvoice() { - return Boolean(this.raw.invoice_journal_id); + return Boolean(this.raw.journal_id); } get isShareable() { diff --git a/addons/point_of_sale/static/src/app/models/pos_order.js b/addons/point_of_sale/static/src/app/models/pos_order.js index f34bfb7dc683ad..9939471c3b39d4 100644 --- a/addons/point_of_sale/static/src/app/models/pos_order.js +++ b/addons/point_of_sale/static/src/app/models/pos_order.js @@ -155,13 +155,13 @@ export class PosOrder extends PosOrderAccounting { if (this.partner_id) { return false; } - const splitPayment = this.payment_ids.some( - (payment) => payment.payment_method_id.split_transactions + const payLater = this.payment_ids.some( + (payment) => payment.payment_method_id.type == "pay_later" ); const invalidPartnerPreset = (this.preset_id?.needsName && !this.floating_order_name) || this.preset_id?.needsPartner; - return invalidPartnerPreset || this.isToInvoice() || Boolean(splitPayment); + return invalidPartnerPreset || this.isToInvoice() || Boolean(payLater); } get presetRequirementsFilled() { @@ -553,7 +553,7 @@ export class PosOrder extends PosOrderAccounting { isPaidWithCash() { return !!this.payment_ids.find(function (pl) { - return pl.payment_method_id.is_cash_count; + return pl.payment_method_id.type === "cash"; }); } diff --git a/addons/point_of_sale/static/src/app/models/pos_payment.js b/addons/point_of_sale/static/src/app/models/pos_payment.js index aa456abbf31d49..a255bdc6d7832e 100644 --- a/addons/point_of_sale/static/src/app/models/pos_payment.js +++ b/addons/point_of_sale/static/src/app/models/pos_payment.js @@ -176,7 +176,7 @@ export class PosPayment extends Base { if (this.payment_interface) { return this.payment_interface.canBeAdjusted(this.uuid); } - return !this.payment_method_id.is_cash_count && !this.useBankQrCode; + return this.payment_method_id.type !== "cash" && !this.useBankQrCode; } async adjustAmount(amount) { diff --git a/addons/point_of_sale/static/src/app/models/res_partner.js b/addons/point_of_sale/static/src/app/models/res_partner.js index 85ccdd83c84059..09f8173e6a47ee 100644 --- a/addons/point_of_sale/static/src/app/models/res_partner.js +++ b/addons/point_of_sale/static/src/app/models/res_partner.js @@ -10,6 +10,14 @@ export class ResPartner extends Base { this._searchString = null; } + get company() { + return this.config.company_id; + } + + get config() { + return this.models["pos.config"].getFirst(); + } + get searchString() { if (this._searchString) { return this._searchString; diff --git a/addons/point_of_sale/static/src/app/screens/payment_screen/payment_screen.js b/addons/point_of_sale/static/src/app/screens/payment_screen/payment_screen.js index 048249166f8bb4..f146d6787f5c81 100644 --- a/addons/point_of_sale/static/src/app/screens/payment_screen/payment_screen.js +++ b/addons/point_of_sale/static/src/app/screens/payment_screen/payment_screen.js @@ -43,6 +43,12 @@ export class PaymentScreen extends Component { onMounted(this.onMounted); } + get isForcedToInvoice() { + // When using customer account, is now mandatory to create an invoice. + const payments = this.currentOrder.payment_ids; + return payments.some((p) => p.payment_method_id.type === "pay_later"); + } + get configPaymentMethods() { return this.pos.config.paymentMethods; } @@ -211,14 +217,6 @@ export class PaymentScreen extends Component { } } async toggleIsToInvoice() { - if (!this.pos.config.canInvoice) { - this.notification.add( - _t("To enable invoice creation, please add a journal for it in the settings."), - { type: "warning" } - ); - return; - } - this.currentOrder.setToInvoice(!this.currentOrder.isToInvoice()); } async addTip() { diff --git a/addons/point_of_sale/static/src/app/screens/payment_screen/payment_screen.xml b/addons/point_of_sale/static/src/app/screens/payment_screen/payment_screen.xml index 4f266d4ab1a994..4275094d2d315d 100644 --- a/addons/point_of_sale/static/src/app/screens/payment_screen/payment_screen.xml +++ b/addons/point_of_sale/static/src/app/screens/payment_screen/payment_screen.xml @@ -18,7 +18,9 @@ selectLine.bind="this.selectPaymentLine" updateSelectedPaymentline.bind="this.updateSelectedPaymentline" isRefundOrder="this.isRefundOrder" /> - +
    + +
    @@ -56,7 +58,9 @@ selectLine.bind="this.selectPaymentLine" updateSelectedPaymentline.bind="this.updateSelectedPaymentline" isRefundOrder="this.isRefundOrder" /> - +
    + +
    @@ -129,14 +133,12 @@
    diff --git a/addons/point_of_sale/static/src/app/services/data_service.js b/addons/point_of_sale/static/src/app/services/data_service.js index 7947cec3c960ce..11bdf0d9b04613 100644 --- a/addons/point_of_sale/static/src/app/services/data_service.js +++ b/addons/point_of_sale/static/src/app/services/data_service.js @@ -341,7 +341,9 @@ export class PosData { for (const model of this.opts.cleanupModels) { const local = localData[model] || []; if (local.length > 0) { - local_records_to_filter[model] = local.map((r) => r.id); + local_records_to_filter[model] = local + .map((r) => r.id) + .filter((id) => typeof id === "number"); } } diff --git a/addons/point_of_sale/static/src/app/services/pos_store.js b/addons/point_of_sale/static/src/app/services/pos_store.js index d105a590f67f04..02ff7b4665c479 100644 --- a/addons/point_of_sale/static/src/app/services/pos_store.js +++ b/addons/point_of_sale/static/src/app/services/pos_store.js @@ -1417,7 +1417,7 @@ export class PosStore extends WithLazyGetterTrap { } } - postSyncAllOrders(orders) {} + async postSyncAllOrders(orders) {} async syncAllOrders(options = {}) { if (this.data.network.offline) { if (options.throw) { diff --git a/addons/point_of_sale/static/src/app/utils/order_payment_validation.js b/addons/point_of_sale/static/src/app/utils/order_payment_validation.js index 2709798fe4a795..3d42edc0bde23b 100644 --- a/addons/point_of_sale/static/src/app/utils/order_payment_validation.js +++ b/addons/point_of_sale/static/src/app/utils/order_payment_validation.js @@ -248,7 +248,7 @@ export default class OrderPaymentValidation { const order = this.pos.getOrder(); const currency = this.pos.currency; for (const payment of order.payment_ids) { - if (!payment.payment_method_id.is_cash_count) { + if (payment.payment_method_id.type !== "cash") { continue; } @@ -334,7 +334,7 @@ export default class OrderPaymentValidation { Math.abs(this.order.priceIncl - this.order.amountPaid + this.order.appliedRounding) > 0.00001 ) { - if (!this.pos.models["pos.payment.method"].some((pm) => pm.is_cash_count)) { + if (!this.pos.models["pos.payment.method"].some((pm) => pm.type === "cash")) { this.pos.dialog.add(AlertDialog, { title: _t("Cannot return change without a cash payment method"), body: _t( @@ -381,7 +381,7 @@ export default class OrderPaymentValidation { async _askForCustomerIfRequired() { const splitPayments = this.order.payment_ids.filter( - (payment) => payment.payment_method_id.split_transactions + (payment) => payment.payment_method_id.type == "pay_later" ); if (splitPayments.length && !this.order.getPartner()) { const paymentMethod = splitPayments[0].payment_method_id; diff --git a/addons/point_of_sale/static/tests/pos/tours/product_screen_tour.js b/addons/point_of_sale/static/tests/pos/tours/product_screen_tour.js index 868c3e35a51cab..00ccc6d07ae559 100644 --- a/addons/point_of_sale/static/tests/pos/tours/product_screen_tour.js +++ b/addons/point_of_sale/static/tests/pos/tours/product_screen_tour.js @@ -1210,7 +1210,7 @@ registry.category("web_tour.tours").add("test_pos_ui_round_globally", { ...["+/-"].map(Numpad.click), ...ProductScreen.selectedOrderlineHasDirect("Test Product 2", "-1.0"), ]), - ProductScreen.totalAmountIs("7,771.01"), + ProductScreen.totalAmountIs("7,771.00"), ProductScreen.clickPayButton(), PaymentScreen.clickPaymentMethod("Bank"), PaymentScreen.clickValidate(), diff --git a/addons/point_of_sale/static/tests/pos/tours/test_pos_accounting_http.js b/addons/point_of_sale/static/tests/pos/tours/test_pos_accounting_http.js new file mode 100644 index 00000000000000..560751e29f8f7a --- /dev/null +++ b/addons/point_of_sale/static/tests/pos/tours/test_pos_accounting_http.js @@ -0,0 +1,31 @@ +/* global posmodel */ + +import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util"; +import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util"; +import { registry } from "@web/core/registry"; +import { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common"; +import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util"; + +registry.category("web_tour.tours").add("test_baseline_between_frontend_and_backend", { + steps: () => + [ + Chrome.startPoS(), + ProductScreen.clickDisplayedProduct("Test Product 1"), + ProductScreen.clickDisplayedProduct("Test Product 2"), + inLeftSide([ + ...["+/-"].map(Numpad.click), + ...ProductScreen.selectedOrderlineHasDirect("Test Product 2", "-1.0"), + ]), + ProductScreen.totalAmountIs("7,771.00"), + { + trigger: "body", + content: "Create an order with a product with dynamic price", + run: async () => { + const data = JSON.stringify(posmodel.getOrder()._computeAllPrices()); + const result = await posmodel.syncAllOrders({ orders: [posmodel.getOrder()] }); + const orderId = result[0].id; + await posmodel.data.call("pos.order", "get_frontend_data", [[orderId], data]); + }, + }, + ].flat(), +}); diff --git a/addons/point_of_sale/static/tests/pos/tours/utils/partner_list_util.js b/addons/point_of_sale/static/tests/pos/tours/utils/partner_list_util.js index daeb772d8a828e..bdfcf07216dca0 100644 --- a/addons/point_of_sale/static/tests/pos/tours/utils/partner_list_util.js +++ b/addons/point_of_sale/static/tests/pos/tours/utils/partner_list_util.js @@ -62,7 +62,6 @@ export function settleCustomerAccount( orderPrefix, orderSuffix = "", checkYear = false, - orderSettlement = false, availability = true ) { const steps = [ @@ -71,10 +70,9 @@ export function settleCustomerAccount( }, clickPartnerOptions(`${partner}`), ]; - const buttonText = orderSettlement ? "Settle orders" : "Settle invoices"; steps.push( ...[ - clickDropDownItemText(buttonText), + clickDropDownItemText("Settle invoices"), clickSettleOrderName(orderPrefix, orderSuffix, checkYear, availability), ] ); diff --git a/addons/point_of_sale/static/tests/unit/accounting/old_tour.test.js b/addons/point_of_sale/static/tests/unit/accounting/old_tour.test.js index 2cbf517606f0f0..ff87049f987f23 100644 --- a/addons/point_of_sale/static/tests/unit/accounting/old_tour.test.js +++ b/addons/point_of_sale/static/tests/unit/accounting/old_tour.test.js @@ -18,8 +18,8 @@ test("[Old Tour] pos_basic_order_01_multi_payment_and_change", async () => { product1.product_variant_ids[0].lst_price = 5.1; product1.taxes_id = []; - const cashPm = store.models["pos.payment.method"].find((pm) => pm.is_cash_count); - const cardPm = store.models["pos.payment.method"].find((pm) => !pm.is_cash_count); + const cashPm = store.models["pos.payment.method"].find((pm) => pm.type === "cash"); + const cardPm = store.models["pos.payment.method"].find((pm) => pm.type === "bank"); const order = store.addNewOrder(); order.pricelist_id = false; diff --git a/addons/point_of_sale/static/tests/unit/accounting/utils.js b/addons/point_of_sale/static/tests/unit/accounting/utils.js index 0e95252b41b017..146f8ec9203c04 100644 --- a/addons/point_of_sale/static/tests/unit/accounting/utils.js +++ b/addons/point_of_sale/static/tests/unit/accounting/utils.js @@ -44,8 +44,8 @@ export const prepareRoundingVals = (store, roundingAmount, roundingMethod, onlyC const config = store.config; const product1 = store.models["product.template"].get(15); const product2 = store.models["product.template"].get(16); - const cashPm = store.models["pos.payment.method"].find((pm) => pm.is_cash_count); - const cardPm = store.models["pos.payment.method"].find((pm) => !pm.is_cash_count); + const cashPm = store.models["pos.payment.method"].find((pm) => pm.type === "cash"); + const cardPm = store.models["pos.payment.method"].find((pm) => pm.type === "bank"); // Changes prices to have a non rounded change product1.list_price = 15.73; diff --git a/addons/point_of_sale/static/tests/unit/data/pos_config.data.js b/addons/point_of_sale/static/tests/unit/data/pos_config.data.js index 8230ccb2bd03ca..be5e0e0b9ab27c 100644 --- a/addons/point_of_sale/static/tests/unit/data/pos_config.data.js +++ b/addons/point_of_sale/static/tests/unit/data/pos_config.data.js @@ -64,7 +64,6 @@ export class PosConfig extends models.ServerModel { use_order_printer: true, is_installed_account_accountant: true, journal_id: 1, - invoice_journal_id: 1, currency_id: 1, iface_big_scrollbars: false, iface_print_auto: false, @@ -86,7 +85,6 @@ export class PosConfig extends models.ServerModel { current_session_id: 1, current_session_state: "opening_control", number_of_rescue_session: 0, - last_session_closing_cash: 0.0, last_session_closing_date: false, pos_session_username: "Administrator", pos_session_state: "opening_control", diff --git a/addons/point_of_sale/static/tests/unit/data/pos_payment_method.data.js b/addons/point_of_sale/static/tests/unit/data/pos_payment_method.data.js index 997f78ad4d6d28..d6fc45dd3dabf5 100644 --- a/addons/point_of_sale/static/tests/unit/data/pos_payment_method.data.js +++ b/addons/point_of_sale/static/tests/unit/data/pos_payment_method.data.js @@ -7,9 +7,7 @@ export class PosPaymentMethod extends models.ServerModel { return [ "id", "name", - "is_cash_count", "payment_provider", - "split_transactions", "type", "image", "sequence", @@ -22,9 +20,7 @@ export class PosPaymentMethod extends models.ServerModel { { id: 2, name: "Card", - is_cash_count: false, payment_provider: false, - split_transactions: false, type: "bank", image: false, sequence: 1, @@ -34,9 +30,7 @@ export class PosPaymentMethod extends models.ServerModel { { id: 3, name: "Customer Account", - is_cash_count: false, payment_provider: false, - split_transactions: true, type: "pay_later", image: false, sequence: 2, @@ -46,9 +40,7 @@ export class PosPaymentMethod extends models.ServerModel { { id: 1, name: "Cash", - is_cash_count: true, payment_provider: false, - split_transactions: false, type: "cash", image: false, sequence: 0, diff --git a/addons/point_of_sale/static/tests/unit/data/pos_session.data.js b/addons/point_of_sale/static/tests/unit/data/pos_session.data.js index 00842706a748e5..3406ce206a6997 100644 --- a/addons/point_of_sale/static/tests/unit/data/pos_session.data.js +++ b/addons/point_of_sale/static/tests/unit/data/pos_session.data.js @@ -58,7 +58,6 @@ export class PosSession extends models.ServerModel { "stop_at", "payment_method_ids", "state", - "cash_register_balance_start", "access_token", ]; } @@ -167,7 +166,6 @@ export class PosSession extends models.ServerModel { stop_at: false, payment_method_ids: [2, 1], state: "opening_control", - cash_register_balance_start: 0.0, access_token: "e09c4843-c913-463a-959d-b9e235881201", }, ]; diff --git a/addons/point_of_sale/static/tests/unit/models/pos_payment.test.js b/addons/point_of_sale/static/tests/unit/models/pos_payment.test.js index 9b87b66de19a8a..2d8d56ab887003 100644 --- a/addons/point_of_sale/static/tests/unit/models/pos_payment.test.js +++ b/addons/point_of_sale/static/tests/unit/models/pos_payment.test.js @@ -168,17 +168,16 @@ test("canBeAdjusted", async () => { const paymentline = createPaymentLine(store, order, card); // no payment interface + is cash - card.is_cash_count = true; + card.type = "cash"; card.payment_method_type = "none"; expect(paymentline.canBeAdjusted()).toBe(false); // no payment interface + is bank qr code - card.is_cash_count = false; + card.type = "bank"; card.payment_method_type = "bank_qr_code"; expect(paymentline.canBeAdjusted()).toBe(false); // no payment interface + is not cash or bank qr code - card.is_cash_count = false; card.payment_method_type = "none"; expect(paymentline.canBeAdjusted()).toBe(true); diff --git a/addons/point_of_sale/static/tests/unit/services/pos_service.test.js b/addons/point_of_sale/static/tests/unit/services/pos_service.test.js index 6ae59585ace20b..151fab394b6e54 100644 --- a/addons/point_of_sale/static/tests/unit/services/pos_service.test.js +++ b/addons/point_of_sale/static/tests/unit/services/pos_service.test.js @@ -534,7 +534,7 @@ describe("pos_store.js", () => { test("getPaymentMethodFmtAmount", async () => { const store = await setupPosEnv(); const order = await getFilledOrder(store); - const cashPm = store.models["pos.payment.method"].find((pm) => pm.is_cash_count); + const cashPm = store.models["pos.payment.method"].find((pm) => pm.type === "cash"); // Case 1: No rounding enabled expect(store.getPaymentMethodFmtAmount(cashPm, order)).toBeEmpty(); diff --git a/addons/point_of_sale/tests/__init__.py b/addons/point_of_sale/tests/__init__.py index fabf3c98820a57..255ebd0e92d7be 100644 --- a/addons/point_of_sale/tests/__init__.py +++ b/addons/point_of_sale/tests/__init__.py @@ -10,17 +10,15 @@ from . import test_pos_invoice_consolidation from . import test_pos_cash_rounding from . import test_pos_setup -from . import test_pos_simple_orders -from . import test_pos_simple_invoiced_orders from . import test_pos_basic_config from . import test_pos_products_with_tax from . import test_pos_margin -from . import test_pos_multiple_receivable_accounts from . import test_pos_other_currency_config -from . import test_pos_with_fiscal_position from . import test_report_pos_order from . import test_report_session from . import test_res_config_settings from . import test_pos_product_variants from . import test_generic_localization from . import test_order_receipt +from . import test_pos_accounting +from . import test_pos_accounting_http diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index 0b5907cf7a4a87..f44031946ffe02 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -59,7 +59,6 @@ def create_pos_configs(self): self.pos_config_usd = self.env['pos.config'].create({ 'name': 'PoS Config USD', 'journal_id': self.company_data['default_journal_sale'].id, - 'invoice_journal_id': self.company_data['default_journal_sale'].id, 'payment_method_ids': [ (4, self.credit_payment_method.id), (4, self.bank_payment_method.id), @@ -118,18 +117,20 @@ def create_account_cash_rounding(self): def create_payment_methods(self): self.cash_payment_method = self.env['pos.payment.method'].create({ 'name': 'Cash', + 'type': 'cash', 'receivable_account_id': self.company_data['default_account_receivable'].id, 'journal_id': self.company_data['default_journal_cash'].id, }) self.bank_payment_method = self.env['pos.payment.method'].create({ 'name': 'Bank', + 'type': 'bank', 'journal_id': self.company_data['default_journal_bank'].id, 'receivable_account_id': self.company_data['default_account_receivable'].id, }) self.credit_payment_method = self.env['pos.payment.method'].create({ 'name': 'Credit', 'receivable_account_id': self.company_data['default_account_receivable'].id, - 'split_transactions': True, + 'type': 'pay_later', }) def create_pos_categories(self): @@ -307,7 +308,7 @@ def create_backend_pos_order(self, data): 'amount_tax': 0, 'amount_return': 0, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), - 'company_id': self.env.company.id, + 'company_id': pos_config.company_id.id, 'session_id': pos_config.current_session_id.id, 'lines': [ Command.create({ @@ -409,7 +410,7 @@ def setUpClass(cls): }) # Set Point of Sale configurations # basic_config - # - derived from 'point_of_sale.pos_config_main' with added invoice_journal_id and credit payment method. + # - derived from 'point_of_sale.pos_config_main' with added journal_id and credit payment method. # other_currency_config # - pos.config set to have currency different from company currency. cls.basic_config = cls._create_basic_config() @@ -448,7 +449,7 @@ def setUpClass(cls): def _create_basic_config(cls): config = cls.env['pos.config'].create({ 'name': 'PoS Shop Test', - 'invoice_journal_id': cls.invoice_journal.id, + 'journal_id': cls.invoice_journal.id, 'available_pricelist_ids': cls.currency_pricelist.ids, 'pricelist_id': cls.currency_pricelist.id, }) @@ -459,32 +460,24 @@ def _create_basic_config(cls): else: cls.cash_pm1 = cls.env['pos.payment.method'].create({ 'name': 'Cash', + 'type': 'cash', 'journal_id': cls.company_data['default_journal_cash'].id, 'receivable_account_id': cls.pos_receivable_cash.id, 'company_id': cls.env.company.id, }) cls.bank_pm1 = cls.env['pos.payment.method'].create({ 'name': 'Bank', + 'type': 'bank', 'journal_id': cls.company_data['default_journal_bank'].id, 'receivable_account_id': cls.pos_receivable_bank.id, 'outstanding_account_id': cls.outstanding_bank.id, 'company_id': cls.env.company.id, }) - cls.cash_split_pm1 = cls.cash_pm1.copy(default={ - 'name': 'Split (Cash) PM', - 'split_transactions': True, - 'journal_id': cls.env['account.journal'].create({ - 'name': "Cash", - 'code': "CSH %s" % config.id, - 'type': 'cash', - }).id - }) cls.bank_split_pm1 = cls.bank_pm1.copy(default={ 'name': 'Split (Bank) PM', - 'split_transactions': True, }) - cls.pay_later_pm = cls.env['pos.payment.method'].create({'name': 'Pay Later', 'split_transactions': True}) - config.write({'payment_method_ids': [(4, cls.cash_split_pm1.id), (4, cls.bank_split_pm1.id), (4, cls.cash_pm1.id), (4, cls.bank_pm1.id), (4, cls.pay_later_pm.id)]}) + cls.pay_later_pm = cls.env['pos.payment.method'].create({'name': 'Pay Later', 'type': 'pay_later'}) + config.write({'payment_method_ids': [(4, cls.bank_split_pm1.id), (4, cls.cash_pm1.id), (4, cls.bank_pm1.id), (4, cls.pay_later_pm.id)]}) return config @classmethod @@ -503,14 +496,6 @@ def _create_other_currency_config(cls): 'sequence': 10, 'currency_id': cls.other_currency.id }) - other_invoice_journal = cls.env['account.journal'].create({ - 'name': 'Customer Invoice Other', - 'type': 'sale', - 'company_id': cls.company.id, - 'code': 'INVO', - 'sequence': 11, - 'currency_id': cls.other_currency.id - }) other_sales_journal = cls.env['account.journal'].create({ 'name':'PoS Sale Other', 'type': 'sale', @@ -533,11 +518,13 @@ def _create_other_currency_config(cls): }) cls.cash_pm2 = cls.env['pos.payment.method'].create({ 'name': 'Cash Other', + 'type': 'cash', 'journal_id': other_cash_journal.id, 'receivable_account_id': cls.pos_receivable_cash.id, }) cls.bank_pm2 = cls.env['pos.payment.method'].create({ 'name': 'Bank Other', + 'type': 'bank', 'journal_id': other_bank_journal.id, 'receivable_account_id': cls.pos_receivable_bank.id, 'outstanding_account_id': cls.outstanding_bank.id, @@ -545,7 +532,6 @@ def _create_other_currency_config(cls): config = cls.env['pos.config'].create({ 'name': 'Shop Other', - 'invoice_journal_id': other_invoice_journal.id, 'journal_id': other_sales_journal.id, 'use_pricelist': True, 'available_pricelist_ids': other_pricelist.ids, @@ -712,19 +698,19 @@ def create_order_line(product, quantity, **kwargs): tax_ids.compute_all(price_unit_after_discount, self.currency, quantity) if tax_ids else { - 'total_excluded': price_unit * quantity, - 'total_included': price_unit * quantity, + 'total_excluded': price_unit_after_discount * quantity, + 'total_included': price_unit_after_discount * quantity, } ) return (0, 0, { 'id': randint(1, 1000000), 'price_unit': price_unit, 'product_id': product.id, - 'price_subtotal': tax_values['total_excluded'], - 'price_subtotal_incl': tax_values['total_included'], + 'price_subtotal': abs(tax_values['total_excluded']), # Must never be negative, qty is used to determine the sign of the amounts + 'price_subtotal_incl': abs(tax_values['total_included']), # Must never be negative, qty is used to determine the sign of the amounts 'qty': quantity, 'tax_ids': [(6, 0, tax_ids.ids)], - **kwargs + **kwargs, }) def create_payment(payment_method, amount): @@ -743,9 +729,17 @@ def create_payment(payment_method, amount): ] # 2. generate the payments - total_amount_incl = sum(line[2]['price_subtotal_incl'] for line in order_lines) + total_amount_incl = 0 + total_amount_base = 0 + for line in order_lines: + line_sign = 1 if line[2]['qty'] >= 0 else -1 + line_price = line[2]['price_subtotal_incl'] * line_sign + base_price = line[2]['price_subtotal'] * line_sign + + total_amount_incl += line_price + total_amount_base += base_price if payments is None: - default_cash_pm = self.config.payment_method_ids.filtered(lambda pm: pm.is_cash_count and not pm.split_transactions)[:1] + default_cash_pm = self.config.payment_method_ids.filtered(lambda pm: pm.type == 'cash')[:1] if not default_cash_pm: raise Exception('There should be a cash payment method set in the pos.config.') payments = [create_payment(default_cash_pm, total_amount_incl)] @@ -756,7 +750,6 @@ def create_payment(payment_method, amount): ] # 3. complete the fields of the order_data - total_amount_base = sum(line[2]['price_subtotal'] for line in order_lines) return { 'amount_paid': sum(payment[2]['amount'] for payment in payments), 'amount_return': 0, @@ -827,10 +820,9 @@ def _run_test(self, args): _logger.info('DONE: Call of before_closing_cb.') self._check_invoice_journal_entries(pos_session, orders_map, expected_values=args['journal_entries_before_closing']) _logger.info('DONE: Checks for journal entries before closing the session.') - cash_payment_method = pos_session.payment_method_ids.filtered('is_cash_count')[:1] + cash_payment_method = pos_session.payment_method_ids.filtered(lambda pm: pm.type == 'cash')[:1] total_cash_payment = sum(pos_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.id == cash_payment_method.id).mapped('amount')) - pos_session.post_closing_cash_details(total_cash_payment) - pos_session.close_session_from_ui() + pos_session.close_session_from_ui({cash_payment_method.id: total_cash_payment}) after_closing_cb = args.get('after_closing_cb') if after_closing_cb: after_closing_cb() @@ -859,7 +851,7 @@ def _check_invoice_journal_entries(self, pos_session, orders_map, expected_value for uid in orders_map: order = orders_map[uid] - if not order.is_invoiced: + if not order.is_singly_invoiced: continue invoice = order.account_move # allow not checking the invoice since pos is not creating the invoices @@ -890,23 +882,16 @@ def _check_session_journal_entries(self, pos_session, expected_values): currency_rounding = pos_session.currency_id.rounding # check expected session journal entry - self._assert_account_move(pos_session.move_id, expected_values['session_journal_entry']) + self._assert_account_move(pos_session.sales_move_id, expected_values['session_journal_entry']) _logger.info("DONE: Check of the session's account move.") # check expected cash journal entries - for statement_line in pos_session.statement_line_ids: + for statement_line in pos_session.bank_statement_line_ids: def statement_line_predicate(args): return tools.float_is_zero(statement_line.amount - args[0], precision_rounding=currency_rounding) self._find_then_assert_values(statement_line.move_id, expected_values['cash_statement'], statement_line_predicate) _logger.info("DONE: Check of cash statement lines.") - # check expected bank payments - for bank_payment in pos_session.bank_payment_ids: - def bank_payment_predicate(args): - return tools.float_is_zero(bank_payment.amount - args[0], precision_rounding=currency_rounding) - self._find_then_assert_values(bank_payment.move_id, expected_values['bank_payments'], bank_payment_predicate) - _logger.info("DONE: Check of bank account payments.") - def _find_then_assert_values(self, account_move, source_of_expected_vals, predicate): expected_move_vals = next(move_vals for args, move_vals in source_of_expected_vals if predicate(args)) self._assert_account_move(account_move, expected_move_vals) diff --git a/addons/point_of_sale/tests/test_backend.py b/addons/point_of_sale/tests/test_backend.py index 435c08bbf2e19e..6a78984344c11f 100644 --- a/addons/point_of_sale/tests/test_backend.py +++ b/addons/point_of_sale/tests/test_backend.py @@ -10,7 +10,7 @@ class TestBackend(TestPoSCommon): def test_onchange_payment_provider(self): - pm = self.env['pos.payment.method'].create({'name': 'Test PM'}) + pm = self.env['pos.payment.method'].create({'name': 'Test PM', 'type': 'bank'}) with patch.object(PosPaymentMethod, '_get_terminal_provider_selection', return_value=[('terminal_1', 'Terminal 1'), ('terminal_2', 'Terminal 2')]), \ patch.object(PosPaymentMethod, '_get_external_qr_provider_selection', return_value=[('qr_1', 'QR Code 1'), ('qr_2', 'QR Code 2')]), \ patch.object(PosPaymentMethod, '_get_cash_machine_selection', return_value=[('cash_1', 'Cash Machine 1'), ('cash_2', 'Cash Machine 2')]): @@ -65,7 +65,7 @@ def test_onchange_payment_provider(self): self.assertEqual(pm.payment_method_type, 'cash_machine') def test_onchange_payment_method_type(self): - pm = self.env['pos.payment.method'].create({'name': 'Test PM'}) + pm = self.env['pos.payment.method'].create({'name': 'Test PM', 'type': 'bank'}) with patch.object(PosPaymentMethod, '_get_terminal_provider_selection', return_value=[('terminal_1', 'Terminal 1'), ('terminal_2', 'Terminal 2')]), \ patch.object(PosPaymentMethod, '_get_external_qr_provider_selection', return_value=[('qr_1', 'QR Code 1'), ('qr_2', 'QR Code 2')]), \ patch.object(PosPaymentMethod, '_get_cash_machine_selection', return_value=[('cash_1', 'Cash Machine 1'), ('cash_2', 'Cash Machine 2')]): diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py index cc358fa025a4fe..089a4539e0964a 100644 --- a/addons/point_of_sale/tests/test_frontend.py +++ b/addons/point_of_sale/tests/test_frontend.py @@ -56,8 +56,13 @@ def with_new_session(self, config=None, user=None): config.with_user(user).open_ui() session = config.current_session_id yield session - session.post_closing_cash_details(0) - session.close_session_from_ui() + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + cash_pm = self.main_pos_config._get_cash_payment_method() + session.close_session_from_ui({ + cash_pm.id: expected_cashbox_amount, + }) @classmethod def setUpClass(cls): @@ -115,6 +120,7 @@ def setUpClass(cls): cls.bank_payment_method = env['pos.payment.method'].create({ 'name': 'Bank', + 'type': 'bank', 'journal_id': cls.bank_journal.id, 'outstanding_account_id': cls.inbound_payment_method_line.payment_account_id.id, }) @@ -579,8 +585,8 @@ def setUpClass(cls): 'tax_regime_selection': True, 'fiscal_position_ids': FP_POS_2M, 'journal_id': test_sale_journal.id, - 'invoice_journal_id': test_sale_journal.id, 'payment_method_ids': [(0, 0, { 'name': 'Cash', + 'type': 'cash', 'journal_id': cash_journal.id, 'receivable_account_id': cls.account_receivable.id, })], @@ -776,67 +782,12 @@ def test_product_information_screen_admin(self): self.main_pos_config.with_user(self.pos_admin).open_ui() self.start_tour("/pos/ui/%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])], - 'categ_id': self.env.ref('product.product_category_services').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/%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, self.env.company.income_account_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, + 'type': 'bank', 'company_id': self.env.company.id, }) self.main_pos_config.write({'payment_method_ids': [(6, 0, bank_pm.ids)]}) @@ -891,26 +842,6 @@ def test_rounding_down(self): self.env["pos.order"].search([('state', '=', 'draft')]).write({'state': 'cancel'}) self.start_tour("/pos/ui/%d" % self.main_pos_config.id, 'PaymentScreenTotalDueWithOverPayment', 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/%d" % self.main_pos_config.id, 'CashClosingDetails', login="pos_user") - self.assertEqual(self.main_pos_config.last_session_closing_cash, 50.0) - 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/%d" % self.main_pos_config.id, 'OrderPaidInCash', login="pos_user") - self.assertEqual(self.main_pos_config.last_session_closing_cash, 25.0) - def test_pos_session_statistics_display(self): """Test that POS session statistics are properly displayed in the UI.""" # For testing `opening_cash` and `paid_orders` in dashboard @@ -1776,11 +1707,14 @@ def test_refund_multiple_products_amounts_compliance(self): refund_order = current_session.order_ids.filtered(lambda order: order.is_refund) self.assertEqual(refund_order.lines[0].price_subtotal, 2 * test_product.list_price) - 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() + closing_data = current_session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + cash_pm = self.main_pos_config._get_cash_payment_method() + current_session.close_session_from_ui({ + cash_pm.id: expected_cashbox_amount, + }) + self.assertEqual(current_session.state, 'closed') report_refund_order, report_order = self.env['report.pos.order'].sudo().search([('order_id', 'in', current_session.order_ids.ids)]) self.assertEqual(report_order.margin, 20.0) @@ -2200,10 +2134,10 @@ def test_pricelist_multi_items_different_qty_thresholds(self): def test_tracking_number_closing_session(self): self.main_pos_config.with_user(self.pos_user).open_ui() - self.start_tour(f"/pos/ui/{self.main_pos_config.id}", 'test_tracking_number_closing_session', login="pos_user") + self.start_tour(f"/pos/ui/{self.main_pos_config.id}", 'test_tracking_number_closing_session', login="accountman") # Change should be given in cash - cash_payment_method = self.main_pos_config.payment_method_ids.filtered(lambda p: p.is_cash_count) + cash_payment_method = self.main_pos_config.payment_method_ids.filtered(lambda p: p.type == 'cash') last_order = self.main_pos_config.current_session_id.order_ids[-1] self.assertRecordValues(last_order.payment_ids.sorted(), [ {'amount': -18.02, 'payment_method_id': cash_payment_method.id, 'is_change': True}, @@ -2218,7 +2152,7 @@ def test_tracking_number_closing_session(self): def test_reload_page_before_payment_with_customer_account(self): self.customer_account_payment_method = self.env['pos.payment.method'].create({ 'name': 'Customer Account', - 'split_transactions': True, + 'type': 'pay_later', }) self.main_pos_config.write({'payment_method_ids': [(6, 0, self.customer_account_payment_method.ids)]}) self.main_pos_config.with_user(self.pos_user).open_ui() @@ -2242,8 +2176,8 @@ def test_cash_in_out(self): self.main_pos_config.with_user(self.pos_user).open_ui() self.start_tour(f"/pos/ui/{self.main_pos_config.id}", 'test_cash_in_out', login="pos_user") - self.assertEqual(len(self.main_pos_config.current_session_id.statement_line_ids), 1, "There should be one cash in/out statement line") - self.assertEqual(self.main_pos_config.current_session_id.statement_line_ids[0].amount, -5, "The cash in/out amount should be -5") + self.assertEqual(len(self.main_pos_config.current_session_id.bank_statement_line_ids), 1, "There should be one cash in/out statement line") + self.assertEqual(self.main_pos_config.current_session_id.bank_statement_line_ids[0].amount, -5, "The cash in/out amount should be -5") def test_edit_paid_order(self): self.main_pos_config.with_user(self.pos_user).open_ui() @@ -2620,19 +2554,17 @@ def test_pos_ui_round_globally(self): 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) + self.assertEqual(pos_session.order_ids[0].payment_ids[0].amount, 7771.0) # Close the session and check the session journal entry. - pos_session.action_pos_session_validate() + pos_session.close_session_from_ui() - lines = pos_session.move_id.line_ids.sorted('balance') + lines = pos_session.move_ids.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) + self.assertEqual(len(lines), 3, "There should be 3 lines in the session journal entry") + self.assertAlmostEqual(lines[0].balance, -6699.14) # Negative line and positive are aggregated + self.assertAlmostEqual(lines[1].balance, -1071.86) # Negative line and positive are aggregated + self.assertAlmostEqual(lines[2].balance, 7771.0) 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") @@ -3064,38 +2996,46 @@ def test_consistent_refund_process_between_frontend_and_backend(self): # Fetch orders created in the current POS session orders = self.env['pos.order'].search([ - ('session_id', '=', self.main_pos_config.current_session_id.id) + ('session_id', '=', self.main_pos_config.current_session_id.id), ]) self.assertEqual(len(orders), 2, "Expected two orders: original and refund.") - original_order = next(o for o in orders if o.amount_total > 0) - frontend_refund_order = next(o for o in orders if o.amount_total < 0) + refunded = orders.filtered(lambda o: o.is_refund) + order = orders - refunded self.assertEqual( - frontend_refund_order.pricelist_id.id, - original_order.pricelist_id.id, - "Refund order pricelist should be the original order's pricelist." + refunded.pricelist_id.id, + order.pricelist_id.id, + "Refund order pricelist should be the original order's pricelist.", ) # Perform refund on order and retrieve the resulting draft refund order - refund_action = original_order.refund() - refund_order = self.env['pos.order'].browse(refund_action['res_id']) + refund_action = order.refund() + backend_refund_order = self.env['pos.order'].browse(refund_action['res_id']) # Validate the refund order is in draft and has correct negative total - self.assertEqual(refund_order.state, 'draft', "Refund order should be in draft state.") - self.assertEqual(refund_order.amount_total, -4, "Refund order total should be -4.") + self.assertEqual(backend_refund_order.state, 'draft', "Refund order should be in draft state.") # Create a payment for the refund using the configured bank method payment_context = { - "active_ids": refund_order.ids, - "active_id": refund_order.id + "active_id": backend_refund_order.id, } refund_payment = self.env['pos.make.payment'].with_context(**payment_context).create({ - 'amount': refund_order.amount_total, + 'amount': backend_refund_order.amount_total, 'payment_method_id': self.bank_payment_method.id, }) # Validate and finalize the refund payment refund_payment.with_context(**payment_context).check() - self.assertEqual(refund_order.state, 'paid', "Refund order should be marked as paid.") + self.assertEqual(backend_refund_order.state, 'paid', "Refund order should be marked as paid.") + + # Lines are always positive even in refunds + self.assertTrue(backend_refund_order.lines.price_subtotal > 0) + self.assertTrue(refunded.lines.price_subtotal > 0) + self.assertTrue(backend_refund_order.lines.price_subtotal_incl > 0) + self.assertTrue(refunded.lines.price_subtotal_incl > 0) + + # Refund order total should be negative (qty = -1) + self.assertTrue(backend_refund_order.amount_total < 0) + self.assertTrue(refunded.amount_total < 0) def test_paid_order_with_archived_product_loads(self): """ Test that a paid order with archived products can be loaded in the POS. """ @@ -3681,7 +3621,7 @@ def test_pos_snooze(self): self.start_pos_tour('test_pos_snooze') def test_set_opening_note_without_cash_method(self): - cash_method = self.main_pos_config.payment_method_ids.filtered(lambda pm: pm.is_cash_count) + cash_method = self.main_pos_config.payment_method_ids.filtered(lambda pm: pm.type == 'cash') self.main_pos_config.payment_method_ids -= cash_method self.main_pos_config.with_user(self.pos_user).open_ui() current_session = self.main_pos_config.current_session_id @@ -3899,11 +3839,17 @@ def _close_pos_session(self): draft_orders = session.order_ids.filtered(lambda o: o.state == 'draft') if draft_orders: draft_orders.action_pos_order_cancel() - session.post_closing_cash_details(0) - session.close_session_from_ui() + cash_pm = self.main_pos_config._get_cash_payment_method() + session.close_session_from_ui({ + cash_pm.id: 0, + }) def assert_pos_orders_and_invoices(self, tour, tests_with_orders): - self._close_pos_session() + if self.main_pos_config.current_session_id: + cash_pm = self.main_pos_config._get_cash_payment_method() + self.main_pos_config.current_session_id.close_session_from_ui({ + cash_pm.id: 0, + }) self.start_pos_tour(tour) orders = self.env['pos.order'].search([('session_id', '=', self.main_pos_config.current_session_id.id)], limit=len(tests_with_orders)) diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index d8dfe01bb1907a..bc26d5a82864cf 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -1,13 +1,11 @@ import odoo -from freezegun import freeze_time from unittest.mock import patch from odoo import fields from odoo.fields import Command -from odoo.tests import Form from datetime import datetime, timedelta from odoo.addons.point_of_sale.tests.common import CommonPosTest -from odoo.exceptions import ValidationError, UserError +from odoo.exceptions import ValidationError @odoo.tests.tagged('post_install', '-at_install') @@ -54,17 +52,18 @@ def test_order_refund(self): self.assertAlmostEqual(order.amount_total, order.amount_paid) self.assertEqual(refund.state, 'paid', "The refund is not marked as paid") - self.assertTrue(refund.payment_ids.payment_method_id.is_cash_count) + self.assertTrue(refund.payment_ids.payment_method_id.type == 'cash') # refund lines should be positive self.assertEqual(refund.lines[0].price_subtotal_incl, 10.0) self.assertEqual(refund.lines[1].price_subtotal_incl, 20.0) current_session = self.pos_config_usd.current_session_id - 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() + closing_data = current_session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + current_session.close_session_from_ui({ + self.cash_payment_method.id: expected_cashbox_amount, + }) self.assertEqual(current_session.state, 'closed') def test_refund_multiple_payment_rounding(self): @@ -101,102 +100,6 @@ def test_refund_multiple_payment_rounding(self): self.assertEqual(refund.amount_paid, -10.0) self.assertEqual(refund.state, 'paid') - def test_order_partial_refund_rounding(self): - """ This test ensures that the refund amound of a partial order corresponds to - the price of the item, without rounding. """ - self.account_cash_rounding_down.rounding = 5.0 - self.pos_config_usd.write({ - 'rounding_method': self.account_cash_rounding_down.id, - 'cash_rounding': True, - }) - - self.pos_config_usd.open_ui() - current_session = self.pos_config_usd.current_session_id - - # order total will be 34.5 with 4.5 taxes excluded, with rounding 10 should be paid - order, _ = self.create_backend_pos_order({ - 'line_data': [ - {'product_id': self.ten_dollars_with_15_excl.product_variant_id.id, 'qty': 3}, - ], - 'payment_data': [ - {'payment_method_id': self.bank_payment_method.id, 'amount': 34.5}, - ], - }) - - self.assertEqual(order._get_rounded_amount(order.amount_total), order.amount_paid) - refund_action = order.refund() - refund = self.env['pos.order'].browse(refund_action['res_id']) - - with Form(refund) as refund_form: - with refund_form.lines.edit(0) as line: - line.qty = 1 - refund = refund_form.save() - - self.assertEqual(refund.amount_total, 10.0) - payment_context = {"active_ids": refund.ids, "active_id": refund.id} - refund_payment = self.env['pos.make.payment'].with_context(**payment_context).create({ - 'amount': refund.amount_total, - 'payment_method_id': self.cash_payment_method.id, - }) - refund_payment.with_context(**payment_context).check() - self.assertEqual(refund.state, 'paid') - current_session.action_pos_session_closing_control() - self.assertEqual(current_session.state, 'closed') - - def test_order_partial_refund(self): - """ The purpose of this test is to make a partial refund of a pos order. - The amount to refund should depend on the article returned and once the - payment made, the refund order should be marked as paid.""" - self.pos_config_usd.open_ui() - current_session = self.pos_config_usd.current_session_id - - # order total will be 30 with 3.52 taxes included - order, _ = self.create_backend_pos_order({ - 'line_data': [ - {'product_id': self.ten_dollars_with_10_incl.product_variant_id.id}, - {'product_id': self.twenty_dollars_with_15_incl.product_variant_id.id}, - ], - 'payment_data': [ - {'payment_method_id': self.cash_payment_method.id, 'amount': 10}, - {'payment_method_id': self.bank_payment_method.id, 'amount': 20}, - ] - }) - - refund_action = order.refund() - refund = self.env['pos.order'].browse(refund_action['res_id']) - - with Form(refund) as refund_form: - with refund_form.lines.edit(0) as line: - line.qty = 0 - refund = refund_form.save() - - self.assertEqual(refund.amount_total, -20.0) - - payment_context = {"active_ids": refund.ids, "active_id": refund.id} - refund_payment = self.env['pos.make.payment'].with_context(**payment_context).create({ - 'amount': refund.amount_total, - 'payment_method_id': self.cash_payment_method.id, - }) - refund_payment.with_context(**payment_context).check() - - self.assertEqual(refund.state, 'paid') - - refund_action = order.refund() - remaining_refund = self.env['pos.order'].browse(refund_action['res_id']) - self.assertEqual(remaining_refund.amount_total, -10.0) - - payment_context = {"active_ids": remaining_refund.ids, "active_id": remaining_refund.id} - refund_payment = self.env['pos.make.payment'].with_context(**payment_context).create({ - 'amount': remaining_refund.amount_total, - 'payment_method_id': self.cash_payment_method.id, - }) - refund_payment.with_context(**payment_context).check() - - self.assertEqual(remaining_refund.state, 'paid') - - current_session.action_pos_session_closing_control() - self.assertEqual(current_session.state, 'closed') - def test_pos_orders_count(self): parent_partner = self.env['res.partner'].create({ 'name': 'Parent Partner', @@ -232,59 +135,6 @@ def test_pos_orders_count(self): self.assertEqual(parent_partner.pos_order_count, 2, "Parent partner should see 2 orders including child’s") self.assertEqual(child_partner.pos_order_count, 1, "Child partner should see only their own order") - 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. - """ - 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, - }) - - order, _ = self.create_backend_pos_order({ - 'order_data': { - 'partner_id': self.partner_mobt.id, - 'pricelist_id': self.partner_mobt.property_product_pricelist.id, - }, - 'line_data': [ - {'product_id': self.ten_dollars_no_tax.product_variant_id.id}, - {'product_id': self.twenty_dollars_no_tax.product_variant_id.id}, - ], - 'payment_data': [ - {'payment_method_id': self.bank_payment_method.id, 'amount': 10}, - {'payment_method_id': self.bank_payment_method.id}, - ], - 'pos_config': self.pos_config_eur, - }) - - self.assertEqual(order.amount_total, 30) - self.assertEqual(order.amount_paid, 30) - self.assertEqual(order.state, 'paid') - current_session = self.pos_config_eur.current_session_id - current_session.action_pos_session_validate() - self.assertTrue(current_session.move_id) - 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, 15.0]): - self.assertAlmostEqual(a, b) - for a, b in zip(sorted(credit_lines), [0.0, 15.0]): - self.assertAlmostEqual(a, b) - for a, b in zip(sorted(amount_currency_lines), [-30, 30]): - self.assertAlmostEqual(a, b) - def test_order_to_invoice_no_tax(self): order, _ = self.create_backend_pos_order({ 'order_data': { @@ -328,46 +178,11 @@ def test_order_to_invoice_no_tax(self): self.pos_config_usd.current_session_id.action_pos_session_closing_control() - def test_order_with_deleted_tax(self): - order, _ = self.create_backend_pos_order({ - 'line_data': [ - {'product_id': self.ten_dollars_with_10_excl.product_variant_id.id}, - ], - }) - - untax, atax = self.compute_tax(self.ten_dollars_with_10_excl.product_variant_id, 10.0) - self.ten_dollars_with_10_excl.taxes_id.active = False - current_session = self.pos_config_usd.current_session_id - payment = self.env['pos.make.payment'].create({ - 'config_id': self.pos_config_usd.id, - 'amount': untax + atax, - 'payment_method_id': self.cash_payment_method.id, - }) - payment.with_context(active_ids=order.ids, active_id=order.id).check() - self.assertEqual(order.state, 'paid', "Order should be in paid state.") - - 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) - - # 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 = current_session._close_session_action(1.0) - wizard = self.env['pos.close.session.wizard'].browse(action['res_id']) - wizard.with_context(action['context']).close_session() - - diff_line = current_session.move_id.line_ids.filtered( - lambda line: line.name == 'Difference at closing PoS session') - self.assertAlmostEqual(diff_line.credit, 1.0, msg="Missing amount of 1.0") - def test_pos_order_invoice_payment_term(self): """ Test that when invoicing a POS order paid with customer account, the partner's payment term is then applied to the invoice. """ self.customer_account_payment_method = self.env['pos.payment.method'].create({ 'name': 'Customer Account', - 'split_transactions': True, + 'type': 'pay_later', }) payment_methods = self.pos_config_usd.payment_method_ids | self.customer_account_payment_method self.pos_config_usd.write({'payment_method_ids': [Command.set(payment_methods.ids)]}) @@ -436,191 +251,6 @@ def test_order_with_different_payments_and_refund(self): }) self.assertEqual(order.account_move.amount_residual, 20) - 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. 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) - """ - tags = self.setup_tags() - with freeze_time('2020-01-01'): - order, _ = self.create_backend_pos_order({ - 'line_data': [ - {'product_id': self.twenty_dollars_with_15_excl.product_variant_id.id}, - ], - 'payment_data': [ - {'payment_method_id': self.bank_payment_method.id, 'amount': 23.0}, - ], - }) - self.pos_config_usd.current_session_id.action_pos_session_closing_control() - - # Check the closing entry. - closing_entry = order.session_move_id - self.assertRecordValues(closing_entry.line_ids.sorted(), [{ - 'balance': -3.0, - 'account_id': self.company_data['default_account_tax_sale'].id, - 'tax_ids': [], - 'tax_tag_ids': tags[1].ids, - 'reconciled': False - }, { - 'balance': -20.0, - 'account_id': self.company_data['default_account_revenue'].id, - 'tax_ids': self.tax_sale_a.ids, - 'tax_tag_ids': tags[0].ids, - 'reconciled': False - }, { - 'balance': 23.0, - 'account_id': self.company_data['default_account_receivable'].id, - 'tax_ids': [], - 'tax_tag_ids': [], - 'reconciled': True - }]) - - with freeze_time('2020-01-03'): - order.partner_id = self.partner_adgu.id - order.action_pos_order_invoice() - - # Check the reverse moves, one for the closing entry, one for the statement lines. - reverse_closing_entries = self.env['account.move'].search([ - ('id', '!=', closing_entry.id), - ('company_id', '=', self.env.company.id), - ('statement_line_id', '=', False), - ('move_type', '=', 'entry'), - ('state', '=', 'posted'), - ]) - self.assertRecordValues(reverse_closing_entries[0].line_ids.sorted(), [{ - 'balance': 3.0, - 'account_id': self.company_data['default_account_tax_sale'].id, - 'tax_ids': [], - 'tax_tag_ids': tags[1].ids, - 'reconciled': False - }, { - 'balance': 20.0, - 'account_id': self.company_data['default_account_revenue'].id, - 'tax_ids': self.tax_sale_a.ids, - 'tax_tag_ids': tags[0].ids, - 'reconciled': False - }, { - 'balance': -23.0, - 'account_id': self.company_data['default_account_receivable'].id, - 'tax_ids': [], - 'tax_tag_ids': [], - 'reconciled': True - }]) - self.assertRecordValues(reverse_closing_entries[2].line_ids.sorted(), [{ - 'balance': -23.0, - 'account_id': self.company_data['default_account_receivable'].id, - 'tax_ids': [], - 'tax_tag_ids': [], - 'reconciled': True - }, { - 'balance': 23.0, - 'account_id': self.company_data['default_account_receivable'].id, - 'tax_ids': [], - 'tax_tag_ids': [], - 'reconciled': True - }]) - - def test_sale_order_postponed_invoicing_storno(self): - """ - Test the flow of creating an invoice later, after the POS session - has been closed and everything has been processed. 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) - This test is the same as test_sale_order_postponed_invoicing but - with the storno feature enabled. - """ - self.env.company.account_storno = True - - tags = self.setup_tags() - with freeze_time('2020-01-01'): - order, _ = self.create_backend_pos_order({ - 'line_data': [ - {'product_id': self.twenty_dollars_with_15_excl.product_variant_id.id}, - ], - 'payment_data': [ - {'payment_method_id': self.bank_payment_method.id, 'amount': 23.0}, - ], - }) - self.pos_config_usd.current_session_id.action_pos_session_closing_control() - - # Check the closing entry. - closing_entry = order.session_move_id - self.assertRecordValues(closing_entry.line_ids.sorted(), [{ - 'balance': -3.0, - 'debit': 0.0, - 'credit': 3.0, - 'account_id': self.company_data['default_account_tax_sale'].id, - 'tax_ids': [], - 'tax_tag_ids': tags[1].ids, - 'reconciled': False, - }, { - 'balance': -20.0, - 'debit': 0.0, - 'credit': 20.0, - 'account_id': self.company_data['default_account_revenue'].id, - 'tax_ids': self.tax_sale_a.ids, - 'tax_tag_ids': tags[0].ids, - 'reconciled': False, - }, { - 'balance': 23.0, - 'debit': 23.0, - 'credit': 0.0, - 'account_id': self.company_data['default_account_receivable'].id, - 'tax_ids': [], - 'tax_tag_ids': [], - 'reconciled': True, - }]) - - with freeze_time('2020-01-03'): - order.partner_id = self.partner_adgu.id - order.action_pos_order_invoice() - - # Check the reverse moves, one for the closing entry, one for the statement lines. - reverse_closing_entries = self.env['account.move'].search([ - ('id', '!=', closing_entry.id), - ('company_id', '=', self.env.company.id), - ('statement_line_id', '=', False), - ('move_type', '=', 'entry'), - ('state', '=', 'posted'), - ]) - self.assertRecordValues(reverse_closing_entries[0].line_ids.sorted(), [{ - 'balance': 3.0, - 'debit': 0.0, - 'credit': -3.0, - 'account_id': self.company_data['default_account_tax_sale'].id, - 'tax_ids': [], - 'tax_tag_ids': tags[1].ids, - 'reconciled': False, - }, { - 'balance': 20.0, - 'debit': 0.0, - 'credit': -20.0, - 'account_id': self.company_data['default_account_revenue'].id, - 'tax_ids': self.tax_sale_a.ids, - 'tax_tag_ids': tags[0].ids, - 'reconciled': False, - }, { - 'balance': -23.0, - 'debit': -23.0, - 'credit': 0.0, - 'account_id': self.company_data['default_account_receivable'].id, - 'tax_ids': [], - 'tax_tag_ids': [], - 'reconciled': True, - }]) - self.assertTrue(all(amount >= 0 for amount in reverse_closing_entries[1:].line_ids.mapped('debit') + reverse_closing_entries[1:].line_ids.mapped('credit')), - "Non-reverse entries should have positive debit or credit amounts.") - def test_order_pos_tax_same_as_company(self): """ Test that when the default_pos_receivable_account and the partner @@ -649,32 +279,6 @@ def test_order_pos_tax_same_as_company(self): self.assertEqual(order.account_move.amount_residual, 0) - def test_journal_entries_category_without_account(self): - # Set company's default accounts to false - self.env.company.income_account_id = False - self.env.company.expense_account_id = False - self.twenty_dollars_with_10_incl.write({ - '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_usd.journal_id.default_account_id = account.id - self.create_backend_pos_order({ - 'line_data': [ - {'product_id': self.twenty_dollars_with_10_incl.product_variant_id.id}, - ], - 'payment_data': [ - {'payment_method_id': self.cash_payment_method.id, 'amount': 20}, - ], - }) - current_session = self.pos_config_usd.current_session_id - current_session.action_pos_session_closing_control() - self.assertEqual(current_session.move_id.line_ids[0].account_id.id, account.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.""" @@ -696,7 +300,7 @@ def test_order_refund_with_invoice(self): ] }) - current_session.action_pos_session_closing_control() + current_session.close_session_from_ui() 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) @@ -744,44 +348,6 @@ def test_refund_rounding_backend(self): self.assertEqual(refund.amount_paid, -25.0) self.assertEqual(current_session.state, 'closed') - def test_pos_branch_account(self): - branch = self.env['res.company'].create({ - 'name': 'Sub Company', - 'parent_id': self.env.company.id, - 'chart_template': self.env.company.chart_template, - 'country_id': self.env.company.country_id.id, - }) - self.env.cr.precommit.run() - self.env.user.group_ids += self.env.ref('point_of_sale.group_pos_manager') - bank_payment_method = self.bank_payment_method.copy() - bank_payment_method.company_id = branch.id - sub_pos_config = self.env['pos.config'].with_company(branch).create({ - 'name': 'Main', - 'journal_id': self.company_data['default_journal_sale'].id, - 'invoice_journal_id': self.company_data['default_journal_sale'].id, - 'payment_method_ids': [(4, bank_payment_method.id)], - }) - - sub_pos_config.open_ui() - current_session = sub_pos_config.current_session_id - self.create_backend_pos_order({ - 'order_data': { - 'partner_id': self.partner_moda.id, - 'pricelist_id': sub_pos_config.pricelist_id.id, - }, - 'line_data': [ - {'product_id': self.ten_dollars_with_10_incl.product_variant_id.id}, - ], - 'payment_data': [ - {'payment_method_id': bank_payment_method.id}, - ], - 'pos_config': sub_pos_config, - }) - - current_session = sub_pos_config.current_session_id - sub_pos_config.current_session_id.action_pos_session_closing_control() - self.assertEqual(current_session.state, 'closed', msg='State of current session should be closed.') - def test_pos_branch_payment_method_config(self): """ This test checks that we don't set a config on a payment method that have different companies. @@ -798,7 +364,6 @@ def test_pos_branch_payment_method_config(self): sub_pos_config = self.env['pos.config'].with_company(branch).create({ 'name': 'Main', 'journal_id': self.company_data['default_journal_sale'].id, - 'invoice_journal_id': self.company_data['default_journal_sale'].id, }) with self.assertRaises(ValidationError, msg="The points of sale for the payment method Bank must belong to its company."): @@ -815,24 +380,13 @@ def test_pos_creation_in_branch(self): "name": "Branch Point of Sale" }) - def test_state_when_closing_register(self): - self.create_backend_pos_order({ - 'line_data': [ - {'product_id': self.ten_dollars_with_10_incl.product_variant_id.id}, - ], - 'payment_data': [ - {'payment_method_id': self.bank_payment_method.id, 'amount': 10}, - ], - }) - current_session = self.pos_config_usd.current_session_id - current_session.action_pos_session_closing_control(bank_payment_method_diffs={self.bank_payment_method.id: 5.00}) - self.assertEqual(current_session.state, 'closed') - def test_change_with_card_only(self): """Test that the change is not skipped if order was overpaid only with card""" self.pos_config_usd.open_ui() pos_session = self.pos_config_usd.current_session_id - cash_payment_method = pos_session.payment_method_ids.filtered('is_cash_count')[:1] + cash_payment_method = pos_session.payment_method_ids.filtered( + lambda pm: pm.type == 'cash', + )[:1] product_order = { 'amount_paid': 500, 'amount_return': -50, @@ -874,12 +428,12 @@ def test_change_with_card_only(self): order_account_move = pos_order.account_move self.assertEqual(order_account_move.amount_total, pos_order.amount_total) - account_moves = self.env['account.move'].search([('pos_payment_ids', 'in', pos_order.payment_ids.ids)]) - self.assertEqual(len(account_moves), 2) - self.assertRecordValues(account_moves.sorted(), [ - {'amount_total': 50}, - {'amount_total': 500}, - ]) + payment_term = order_account_move.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + payment_amount = payment_term.mapped('amount_currency') + self.assertEqual(len(payment_term), 2) + self.assertEqual(payment_amount, [500.0, -50.0]) def test_refund_qty_refund_cancel(self): """ @@ -1047,114 +601,6 @@ def test_pos_order_partner_bank_id(self): "Invoice should not use journal bank account when not allowed." ) - def test_order_invoiced_after_session_closed(self): - """Test that an order can be invoiced after its session is closed. - Scenario: - 1. Create a POS session and two orders: - - Order A: Not to be invoiced immediately. - - Order B: To be invoiced immediately. - 2. Close the POS session. - 3. Ensure: - - Both orders have `reversed_move_ids` unset. - 4. Assign a partner to Order A and invoice it AFTER the session is closed. - - Confirm that `reversed_move_ids` is set accordingly. - """ - order_data = { - 'line_data': [ - {'product_id': self.ten_dollars_with_10_incl.product_variant_id.id}, - {'product_id': self.twenty_dollars_with_10_incl.product_variant_id.id}, - ], - 'payment_data': [ - {'payment_method_id': self.bank_payment_method.id, 'amount': 30}, - ], - } - - self.pos_config_usd.open_ui() - current_session = self.pos_config_usd.current_session_id - - order_no_invoice, _ = self.create_backend_pos_order({**order_data, 'to_invoice': False, 'partner_id': False}) - order_invoiced_immediate, _ = self.create_backend_pos_order({**order_data, 'to_invoice': True, 'partner_id': self.partner.id}) - - total_cash_payment = sum( - current_session.mapped('order_ids.payment_ids') - .filtered(lambda p: p.payment_method_id.type == 'cash') - .mapped('amount') - ) - current_session.post_closing_cash_details(total_cash_payment) - current_session.close_session_from_ui() - - # Ensure the session is closed - self.assertEqual(current_session.state, 'closed') - - # Initial state: no reversal moves for both orders - self.assertFalse(order_no_invoice.reversed_move_ids, - "Order with 'to_invoice' = False should have no reversal moves after session is closed.") - self.assertFalse(order_invoiced_immediate.reversed_move_ids, - "Immediate invoiced order should have no reversal moves after session is closed.") - - # Now, set a partner and invoice the order after the session is closed - order_no_invoice.partner_id = self.partner - order_no_invoice.action_pos_order_invoice() - - # Confirm that reversal move(s) are now set - reversal_moves = self.env['account.move'].search([('reversed_pos_order_id', '=', order_no_invoice.id)]) - self.assertEqual(order_no_invoice.reversed_move_ids, reversal_moves, - "Reversal move should be set for the order invoiced after the session is closed.") - - def test_payment_difference_accounting_items(self): - """Verify that the amount of the accounting items are correct when closing a session with a payment difference.""" - self.product1 = self.env['product.product'].create({ - 'name': 'Test Product', - 'lst_price': 100, - }) - # Make a sale paid by bank - self.pos_config_usd.open_ui() - session_id = self.pos_config_usd.current_session_id - order = self.env['pos.order'].create({ - 'company_id': self.env.company.id, - 'session_id': session_id.id, - 'partner_id': False, - 'lines': [Command.create({ - 'name': 'OL/0001', - 'product_id': self.product1.id, - 'price_unit': 100.00, - 'discount': 0, - 'qty': 1, - 'tax_ids': False, - 'price_subtotal': 100.00, - 'price_subtotal_incl': 100.00, - })], - 'pricelist_id': self.pos_config_usd.pricelist_id.id, - 'amount_paid': 100.00, - 'amount_total': 100.00, - 'amount_tax': 0.0, - 'amount_return': 0.0, - 'to_invoice': False, - }) - - # Make payment - payment_context = {"active_ids": order.ids, "active_id": order.id} - order_payment = self.env['pos.make.payment'].with_context(**payment_context).create({ - 'amount': order.amount_total, - 'payment_method_id': self.bank_payment_method.id - }) - order_payment.with_context(**payment_context).check() - - session_id.action_pos_session_closing_control(bank_payment_method_diffs={self.bank_payment_method.id: -10.00}) - self.bank_payment_move = session_id._get_related_account_moves().filtered(lambda move: 'Combine Bank' in move.ref) - self.assertRecordValues(self.bank_payment_move.line_ids.sorted('balance'), [{ - 'balance': -100.0, - 'account_id': self.bank_payment_method.receivable_account_id.id, - }, - { - 'balance': 10.0, - 'account_id': self.bank_payment_method.journal_id.loss_account_id.id, - }, - { - 'balance': 90.0, - 'account_id': self.bank_payment_move.payment_ids.outstanding_account_id.id, - }]) - def test_invoice_rounding_overpaid_backend(self): rouding_method = self.env['account.cash.rounding'].create({ 'name': 'Rounding up', @@ -1180,9 +626,9 @@ def test_invoice_rounding_overpaid_backend(self): current_session = self.pos_config_usd.current_session_id pos_order_data = { - 'amount_paid': 149.99, + 'amount_paid': 150, # Should correspond to the total paid in payment_ids 'amount_tax': 0, - 'amount_return': 0.01, + 'amount_return': 0, # Is computed by sync_from_ui 'amount_total': 149.99, 'date_order': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, @@ -1214,9 +660,12 @@ def test_invoice_rounding_overpaid_backend(self): } self.env['pos.order'].sync_from_ui([pos_order_data]) - 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() + closing_data = current_session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + current_session.close_session_from_ui({ + self.cash_payment_method.id: expected_cashbox_amount, + }) pos_order = self.env['pos.order'].search([]) pos_order.action_pos_order_invoice() @@ -1259,26 +708,6 @@ def test_search_paid_order_ids(self): self.assertNotIn(paid_order_1.id, order_ids) self.assertIn(paid_order_2.id, order_ids) - def test_session_name_gap(self): - self.pos_config_usd.open_ui() - session = self.pos_config_usd.current_session_id - session.set_opening_control(0, None) - current_session_name = session.name - session.action_pos_session_closing_control() - - self.pos_config_usd.open_ui() - session = self.pos_config_usd.current_session_id - - def _post_cash_details_message_patch(*_args, **_kwargs): - raise UserError('Test Error') - - with patch.object(self.env.registry.models['pos.session'], "_post_cash_details_message", _post_cash_details_message_patch): - with self.assertRaises(UserError): - session.set_opening_control(0, None) - - session.set_opening_control(0, None) - self.assertEqual(int(session.name.split('/')[1]), int(current_session_name.split('/')[1]) + 1) - def test_open_ui_missing_country(self): """ Test that a POS can not be opened if it has no country """ self.pos_config_usd.company_id.account_fiscal_country_id = False @@ -1309,7 +738,7 @@ def test_branch_company_access_cost_currency_id(self): 'name': 'Main', 'company_id': branch.id, }) - config.payment_method_ids.filtered(lambda pm: pm.is_cash_count).unlink() + config.payment_method_ids.filtered(lambda pm: pm.type == 'cash').unlink() config.open_ui() current_session = config.current_session_id @@ -1374,56 +803,6 @@ def test_delete_res_partner_linked_to_pos_order(self): with self.assertRaises(ValidationError, msg='You cannot delete a customer that has point of sales orders. You can archive it instead.'): partner.unlink() - def test_split_payment_linked_to_accounting_partner(self): - self.bank_payment_method.write({'split_transactions': True}) - self.pos_config_usd.open_ui() - current_session = self.pos_config_usd.current_session_id - - child_partner = self.env['res.partner'].create({ - 'name': 'partner1 child', - 'parent_id': self.partner.id - }) - product_order = { - '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, - 'price_unit': 750.0, - 'product_id': self.product.id, - 'price_subtotal': 750.0, - 'price_subtotal_incl': 750.0, - 'tax_ids': [[6, False, []]], - 'qty': 1, - }]], - 'name': 'Order 12345-123-1234', - 'partner_id': child_partner.id, - 'session_id': current_session.id, - 'sequence_number': 2, - 'payment_ids': [[0, 0, { - 'amount': 750, - 'name': fields.Datetime.now(), - 'payment_method_id': self.bank_payment_method.id - }]], - 'uuid': '12345-123-1234', - 'user_id': self.env.uid, - 'to_invoice': False} - - self.env['pos.order'].sync_from_ui([product_order]) - current_session.close_session_from_ui() - order_balance = current_session.move_id.line_ids.filtered( - lambda l: l.account_id.account_type == "asset_receivable" - and l.partner_id == self.partner - ).balance - payment_balance = current_session.bank_payment_ids.move_id.line_ids.filtered( - lambda l: l.account_id.account_type == "asset_receivable" - and l.partner_id == self.partner - ).balance - self.assertEqual(order_balance + payment_balance, 0) - def test_draft_orders_products_loading(self): """ Test that products are correctly loaded when limited product loading is enabled and there are draft orders. """ self.env['ir.config_parameter'].sudo().set_int('point_of_sale.limited_product_count', 1) @@ -1514,40 +893,9 @@ def test_payment_method_sequence(self): methods = self.env['pos.payment.method'].browse(pm_ids)[:3] self.assertEqual(methods.mapped('name'), ['Cash', 'Card', 'Customer Account']) self.assertEqual(methods.mapped('sequence'), [1, 2, 4]) - new_pm = self.env['pos.payment.method'].create({'name': 'Quick Pay'}) + new_pm = self.env['pos.payment.method'].create({'name': 'Quick Pay', 'type': 'bank'}) self.assertEqual(new_pm.sequence, 5) - def test_order_invoiced_customer_account_after_session_closed(self): - """Test that an order paid via customer account can be invoiced after its session is closed. - Then make sure that the reversal move is reconciled with the PoS session account move line so that only the invoice remains open. - """ - order_data = { - 'line_data': [ - {'product_id': self.twenty_dollars_with_10_incl.product_variant_id.id}, - ], - 'payment_data': [ - {'payment_method_id': self.credit_payment_method.id, 'amount': 20}, - ], - 'order_data': { - 'partner_id': self.partner.id, - }, - } - - self.pos_config_usd.open_ui() - current_session = self.pos_config_usd.current_session_id - - order_no_invoice, _ = self.create_backend_pos_order({**order_data, 'to_invoice': False}) - - current_session.close_session_from_ui() - self.assertEqual(current_session.state, 'closed') - - order_no_invoice.action_pos_order_invoice() - customer_account_receivable_entry = current_session.move_id.line_ids.filtered(lambda l: l.partner_id == self.partner) - reversal_receivable_entry = order_no_invoice.reversed_move_ids.line_ids.filtered(lambda l: l.account_id == self.partner.property_account_receivable_id) - self.assertTrue(customer_account_receivable_entry.reconciled) - self.assertTrue(reversal_receivable_entry.reconciled) - self.assertEqual(customer_account_receivable_entry.reconciled_lines_ids, reversal_receivable_entry) - def test_add_two_lines_with_same_uuid_through_sync_from_ui(self): """Test that adding two lines with the same UUID doesn't cause issues.""" self.pos_config_usd.open_ui() @@ -1590,101 +938,3 @@ def test_add_two_lines_with_same_uuid_through_sync_from_ui(self): }]) self.assertEqual(len(order.lines), 1, "Two lines with the same UUID were created") self.assertEqual(order.lines[0].qty, 2, "The quantity of the line should have been updated to 2") - - def test_manual_refund_negative_qty_invoice_creates_credit_note(self): - """Invoicing a POS order created with negative qty (manual refund, no Refund action) - must create a credit note (RINV/out_refund), not a customer invoice (INV).""" - self.pos_config_usd.open_ui() - - # Create an order with negative qty only (no Refund action → is_refund stays False) - order, _ = self.create_backend_pos_order({ - 'order_data': { - 'partner_id': self.partner_mobt.id, - 'pricelist_id': self.pos_config_usd.pricelist_id.id, - }, - 'line_data': [ - {'product_id': self.ten_dollars_no_tax.product_variant_id.id, 'qty': -1}, - ], - 'payment_data': [ - {'payment_method_id': self.cash_payment_method.id, 'amount': -10}, - ], - }) - - self.assertEqual(order.state, 'paid') - self.assertLess(order.amount_total, 0, 'Order total should be negative (manual refund).') - self.assertFalse(order.is_refund, 'Order was not created via Refund action.') - - order.action_pos_order_invoice() - - self.assertTrue(order.account_move, 'An invoice/credit note should be created.') - self.assertEqual( - order.account_move.move_type, - 'out_refund', - 'Invoicing a manual refund (negative qty) must create a credit note (RINV), not a customer invoice.', - ) - - def test_pos_payment_direction_and_accounts(self): - """Ensure POS payments create correct inbound/outbound payments and accounts.""" - - def _do_pos_transaction(amount, split, index): - self.bank_payment_method.write({'split_transactions': split}) - self.pos_config_usd.open_ui() - current_session = self.pos_config_usd.current_session_id - product_order = { - 'amount_paid': amount, - 'amount_tax': 0, - 'amount_return': 0, - 'amount_total': amount, - 'date_order': fields.Datetime.to_string(fields.Datetime.now()), - 'lines': [[0, 0, { - 'price_unit': 100.0, - 'product_id': self.product.id, - 'price_subtotal': amount, - 'price_subtotal_incl': amount, - 'qty': 1 if amount > 0 else -1, - }]], - 'name': f'Order {index}', - 'partner_id': self.partner.id, - 'session_id': current_session.id, - 'payment_ids': [[0, 0, { - 'amount': amount, - 'payment_method_id': self.bank_payment_method.id - }]], - 'uuid': f'12345-123-1253{index}', - 'user_id': self.env.uid, - 'to_invoice': False - } - self.env['pos.order'].sync_from_ui([product_order]) - current_session.close_session_from_ui() - return current_session - - self.bank_payment_method.outstanding_account_id = self.inbound_payment_method_line.payment_account_id.id - session_ids = [ - _do_pos_transaction(amount, split, idx).id - for idx, (amount, split) in enumerate([(100, False), (-100, False), (100, True), (-100, True)]) - ] - self.assertRecordValues( - self.env['account.payment'].search([('pos_session_id', 'in', session_ids)], order='id'), - [ - { - "payment_type": "inbound", - "outstanding_account_id": self.bank_payment_method.outstanding_account_id.id, - "destination_account_id": self.bank_payment_method.receivable_account_id.id, - }, - { - "payment_type": "outbound", - "outstanding_account_id": self.bank_payment_method.receivable_account_id.id, - "destination_account_id": self.bank_payment_method.outstanding_account_id.id, - }, - { - "payment_type": "inbound", - "outstanding_account_id": self.bank_payment_method.outstanding_account_id.id, - "destination_account_id": self.bank_payment_method.receivable_account_id.id, - }, - { - "payment_type": "outbound", - "outstanding_account_id": self.bank_payment_method.receivable_account_id.id, - "destination_account_id": self.bank_payment_method.outstanding_account_id.id, - }, - ], - ) diff --git a/addons/point_of_sale/tests/test_pos_accounting.py b/addons/point_of_sale/tests/test_pos_accounting.py new file mode 100644 index 00000000000000..c3fd213ae412e6 --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_accounting.py @@ -0,0 +1,1831 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import Command, fields +from odoo.exceptions import UserError +from odoo.tests import Form, freeze_time + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +class TestPosAccounting(AccountTestInvoicingCommon): + + @classmethod + def _get_main_company(self): + return self.company_data['company'] + + @classmethod + def setUpClass(self): + super().setUpClass() + self.main_company = self._get_main_company() + pos_manager = self.env.ref('point_of_sale.group_pos_manager') + self.env.user.group_ids += pos_manager + + # Create journals + self.bank_journal = self.env['account.journal'].create({ + 'name': 'Bank Test', + 'type': 'bank', + 'company_id': self.main_company.id, + 'code': 'BNK', + 'sequence': 10, + }) + self.cash_journal = self.env['account.journal'].create({ + 'name': 'Cash Test', + 'type': 'cash', + 'company_id': self.main_company.id, + 'code': 'CSH', + 'sequence': 10, + }) + self.config_sale_journal = self.env['account.journal'].create({ + 'name': 'PoS Sale', + 'type': 'sale', + 'code': 'POSS', + 'company_id': self.company.id, + 'sequence': 12, + }) + + # Accounts + self.bank_outstanding_account = self.copy_account( + self.inbound_payment_method_line.payment_account_id, + {'name': 'Outstanding Bank'}, + ) + + # Create payment methods + self.cash_pm = self.env['pos.payment.method'].create({ + 'name': 'Cash', + 'type': 'cash', + 'journal_id': self.cash_journal.id, + }) + self.customer_pm = self.env['pos.payment.method'].create({ + 'name': 'Customer Account', + 'type': 'pay_later', + }) + self.bank_pm = self.env['pos.payment.method'].create({ + 'name': 'Bank', + 'type': 'bank', + 'journal_id': self.bank_journal.id, + 'outstanding_account_id': self.bank_outstanding_account.id, + }) + + # Create taxes with different rates + self.tax_received_account = self.env['account.account'].create({ + 'name': 'TAX_BASE', + 'code': 'TBASE', + 'account_type': 'asset_current', + }) + tax_repartition = [ + (0, 0, {'repartition_type': 'base'}), + (0, 0, { + 'repartition_type': 'tax', + 'account_id': self.tax_received_account.id, + }), + ] + self.tax_6 = self.env['account.tax'].create({ + 'name': 'Tax 6%', + 'amount_type': 'percent', + 'amount': 6, + 'invoice_repartition_line_ids': tax_repartition, + 'refund_repartition_line_ids': tax_repartition, + }) + self.tax_12 = self.env['account.tax'].create({ + 'name': 'Tax 12%', + 'amount_type': 'percent', + 'amount': 12, + 'invoice_repartition_line_ids': tax_repartition, + 'refund_repartition_line_ids': tax_repartition, + }) + self.tax_21 = self.env['account.tax'].create({ + 'name': 'Tax 21%', + 'amount_type': 'percent', + 'amount': 21, + 'invoice_repartition_line_ids': tax_repartition, + 'refund_repartition_line_ids': tax_repartition, + }) + self.tax_fixed = self.env['account.tax'].create({ + 'name': 'fixed amount tax', + 'amount_type': 'fixed', + 'amount': 1, + 'price_include_override': 'tax_excluded', + 'invoice_repartition_line_ids': tax_repartition, + 'refund_repartition_line_ids': tax_repartition, + }) + + # Create products with different tax configurations + self.product_6 = self.env['product.product'].create({ + 'name': 'Product 6%', + 'type': 'consu', + 'qty_available': 100, + 'is_storable': True, + 'list_price': 10, + 'taxes_id': [(6, 0, [self.tax_6.id])], + 'available_in_pos': True, + }) + self.product_12 = self.env['product.product'].create({ + 'name': 'Product 12%', + 'type': 'consu', + 'qty_available': 100, + 'is_storable': True, + 'list_price': 10, + 'taxes_id': [(6, 0, [self.tax_12.id])], + 'available_in_pos': True, + }) + self.product_21 = self.env['product.product'].create({ + 'name': 'Product 21%', + 'type': 'consu', + 'is_storable': True, + 'list_price': 10, + 'taxes_id': [(6, 0, [self.tax_21.id])], + 'available_in_pos': True, + }) + self.product_6_12 = self.env['product.product'].create({ + 'name': 'Product 6% + 12%', + 'type': 'consu', + 'is_storable': True, + 'list_price': 10, + 'taxes_id': [(6, 0, [self.tax_6.id, self.tax_12.id])], + 'available_in_pos': True, + }) + taxes = [self.tax_6.id, self.tax_12.id, self.tax_21.id] + self.product_6_12_21 = self.env['product.product'].create({ + 'name': 'Product 6% + 12% + 21%', + 'type': 'consu', + 'is_storable': True, + 'list_price': 10, + 'taxes_id': [(6, 0, taxes)], + 'available_in_pos': True, + }) + + # Create different partners to use customer account + self.partner_1 = self.env['res.partner'].create({ + 'name': 'Partner 1', + }) + self.partner_2 = self.env['res.partner'].create({ + 'name': 'Partner 2', + }) + + # Create the main PoS configuration used in the tests + revenue = self.company_data['default_account_revenue'].id + expense = self.company_data['default_account_expense'].id + self.rounding_method = self.env['account.cash.rounding'].create({ + 'name': 'Rounding up', + 'rounding': 0.05, + 'rounding_method': 'UP', + 'profit_account_id': revenue, + 'loss_account_id': expense, + }) + self.pos_config = self.env['pos.config'].create({ + 'name': 'PoS Config', + 'journal_id': self.config_sale_journal.id, + 'payment_method_ids': [ + (4, self.cash_pm.id), + (4, self.customer_pm.id), + (4, self.bank_pm.id), + ], + }) + + def get_pos_session(self): + return self.pos_config.current_session_id + + def open_pos_session(self, opening=0, note=""): + self.pos_config.open_ui() + session = self.get_pos_session() + session.set_opening_control(opening, note) + self.assertEqual(session.state, 'opened') + return session + + def close_session(self, amount=0, note=""): + session = self.get_pos_session() + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + session.close_session_from_ui({self.cash_pm.id: expected_cashbox_amount}) + self.assertEqual(session.state, 'closed') + return session + + def create_pos_order(self, payment_method=[], products=[], extra_data={}): + order = { + 'amount_total': 0, + 'amount_paid': 0, + 'amount_tax': 0, + 'amount_return': 0, + 'state': 'draft', + 'date_order': fields.Datetime.to_string(fields.Datetime.now()), + 'company_id': self.env.company.id, + 'session_id': self.get_pos_session().id, + 'lines': [Command.create({ + 'product_id': product.id, + 'price_unit': product.lst_price, + 'price_subtotal': product.lst_price, + 'tax_ids': [(6, 0, product.taxes_id.ids)], + 'price_subtotal_incl': 0, + **extra_data, + }) for [product, extra_data] in products], + 'payment_ids': [ + Command.create({ + 'payment_method_id': pm.id, + **data, + }) for [pm, data] in payment_method + ], + **extra_data, + } + + data = self.env['pos.order'].sync_from_ui([order]) + order = self.env['pos.order'].browse(data['pos.order'][0]['id']) + order._compute_prices() + if len(payment_method): + order_ctx = order.with_context({'generate_pdf': False}) + order_ctx._process_saved_order(False) + return order + + def test_cash_closing_data_do_not_take_into_account_invoiced_order(self): + session = self.open_pos_session() + self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + + # Payment amount doesn't need to be taken into account for + # invoiced orders since the statement line is already generated + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + self.assertEqual(cash_details['amount'], 10.6) + self.assertEqual(cash_details['payment_amount'], 0) + + def test_invoiced_order_are_on_partner_receivable_account(self): + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + self.close_session() + move = order.account_move + self.assertNotEqual(move, session.sales_move_id) + + def test_cash_statement_opening_and_closing_consistency(self): + def open_and_close_session_with_cash_amounts(start, end): + session = self.open_pos_session(start) + session.close_session_from_ui({self.cash_pm.id: end}) + self.assertEqual(session.state, 'closed') + return self.env['account.bank.statement'].browse( + session.bank_statement_id.id, + ) + + # OPEN WITH MORE CLOSE EQUAL + # Initial balance is 0 + # - Opening 10 line +10 balance 10 + # - Closing 10 line N/A balance 10 + statement = open_and_close_session_with_cash_amounts(10, 10) + self.assertEqual(len(statement.line_ids), 1) + self.assertEqual(statement.line_ids.amount, 10) + self.assertEqual(statement.balance_end_real, 10) + + # OPEN WITH LESS CLOSE WITH MORE + # Initial balance is 10 + # - Opening 5 line -5 balance 5 + # - Closing 10 line +5 balance 10 + statement = open_and_close_session_with_cash_amounts(5, 10) + self.assertEqual(len(statement.line_ids), 2) + self.assertEqual(statement.line_ids[0].amount, -5) + self.assertEqual(statement.line_ids[1].amount, 5) + self.assertEqual(statement.balance_end_real, 10) + + # OPEN WITH MORE CLOSE WITH LESS + # Initial balance is 10 + # - Opening 20 line +10 balance 20 + # - Closing 10 line -10 balance 10 + statement = open_and_close_session_with_cash_amounts(20, 10) + self.assertEqual(len(statement.line_ids), 2) + self.assertEqual(statement.line_ids[0].amount, 10) + self.assertEqual(statement.line_ids[1].amount, -10) + self.assertEqual(statement.balance_end_real, 10) + + # OPEN EQUAL CLOSE EQUAL + # Initial balance is 10 + # - Opening 10 line N/A balance 10 + # - Closing 10 line N/A balance 10 + statement = open_and_close_session_with_cash_amounts(10, 10) + self.assertEqual(len(statement.line_ids), 0) + self.assertEqual(statement.balance_end_real, 10) + + # OPEN EQUAL CLOSE WITH MORE + # Initial balance is 10 + # - Opening 10 line N/A balance 10 + # - Closing 20 line +10 balance 20 + statement = open_and_close_session_with_cash_amounts(10, 20) + self.assertEqual(len(statement.line_ids), 1) + self.assertEqual(statement.line_ids[0].amount, 10) + self.assertEqual(statement.balance_end_real, 20) + + # OPEN EQUAL CLOSE WITH LESS + # Initial balance is 20 + # - Opening 20 line N/A balance 20 + # - Closing 15 line -5 balance 15 + statement = open_and_close_session_with_cash_amounts(20, 15) + self.assertEqual(len(statement.line_ids), 1) + self.assertEqual(statement.line_ids[0].amount, -5) + self.assertEqual(statement.balance_end_real, 15) + + # OPEN WITH LESS CLOSE EQUAL + # Initial balance is 15 + # - Opening 10 line -5 balance 10 + # - Closing 10 line N/A balance 10 + statement = open_and_close_session_with_cash_amounts(10, 10) + self.assertEqual(len(statement.line_ids), 1) + self.assertEqual(statement.line_ids[0].amount, -5) + self.assertEqual(statement.balance_end_real, 10) + + # OPEN WITH LESS CLOSE WITH LESS + # Initial balance is 10 + # - Opening 5 line -5 balance 5 + # - Closing 3 line -2 balance 3 + statement = open_and_close_session_with_cash_amounts(5, 3) + self.assertEqual(len(statement.line_ids), 2) + self.assertEqual(statement.line_ids[0].amount, -5) + self.assertEqual(statement.line_ids[1].amount, -2) + self.assertEqual(statement.balance_end_real, 3) + + # OPEN WITH MORE CLOSE WITH MORE + # Initial balance is 3 + # - Opening 8 line +5 balance 8 + # - Closing 12 line +4 balance 12 + statement = open_and_close_session_with_cash_amounts(8, 12) + self.assertEqual(len(statement.line_ids), 2) + self.assertEqual(statement.line_ids[0].amount, 5) + self.assertEqual(statement.line_ids[1].amount, 4) + self.assertEqual(statement.balance_end_real, 12) + + def test_tax_change_blocked_when_open_pos_session(self): + """ + Changing a POS sale tax must be blocked when a POS session + is open, this test also check if the tax is correctly marked + as used when it's part of a PoS order line + """ + session = self.open_pos_session() + tax_pos = self.product_6.taxes_id + self.assertFalse(tax_pos.is_used) + + order = self.create_pos_order( + products=[[self.product_6, {}]], + extra_data={'state': 'draft'}, + ) + self.assertEqual(order.lines.tax_ids, self.tax_6) # sanity check to ensure the order line has the correct tax + self.assertEqual(order.session_id, session) # sanity check to ensure the order is linked to the current session + with self.assertRaises(UserError): + self.tax_6.write({ + 'price_include_override': 'tax_included', + }) + + tax_pos.invalidate_model(fnames=['is_used']) + self.assertTrue(tax_pos.is_used) + + def test_classic_order(self): + session = self.open_pos_session() + customer_order = self.create_pos_order( + payment_method=[[self.customer_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + extra_data={'partner_id': self.partner_1.id}, + ) + + self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + ) + + self.create_pos_order( + payment_method=[[self.bank_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + ) + + self.close_session() + sale_move = session.move_ids + cash_statement = self.cash_pm.journal_id.last_statement_id + self.assertEqual(cash_statement, session.bank_statement_id) # Cash statement should be the one linked to the session + self.assertEqual(len(sale_move.line_ids), 4) # 3 payment_term + 1 product + 1 tax + self.assertEqual(sale_move.amount_total, 21.2) # 10 + 6% tax * 2 orders (customer account order is not taken into account) + self.assertEqual(sale_move.amount_tax, 1.2) # 6% tax on 30 + self.assertEqual(len(cash_statement.line_ids), 1) # Only one line in the cash statement since the 3 orders are merged into one statement line + self.assertEqual(cash_statement.line_ids.amount, 10.6) # 10 + 6% tax from the cash order + self.assertEqual(cash_statement.is_complete, True) + + # Customer account invoice + invoice = self.env['account.move'].search([ + ('move_type', '=', 'out_invoice'), + ('partner_id', '=', self.partner_1.id), + ]) + self.assertEqual(invoice.amount_total, 10.6) # 10 + 6% tax from the customer account order + self.assertEqual(invoice.amount_tax, 0.6) # 6% tax on 10 + self.assertEqual(invoice.amount_residual, 10.6) # Not yet paid + self.assertEqual(invoice.amount_paid, 0.0) # Not yet paid + self.assertEqual(customer_order.to_invoice, True) # Forced to True since the order is paid with a customer account + + def test_cash_statement_line(self): + session = self.open_pos_session() + + # Cash payment of 10.6 (10 + 6% tax) + self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + ) + + # Cash payment of 11.2 (10 + 12% tax) + self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 11.2}]], + products=[[self.product_12, {}]], + ) + + # Cash payment of 12.0 (10 + 21% tax) + self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 12.1}]], + products=[[self.product_21, {}]], + ) + + self.close_session() + cash_statement = self.cash_pm.journal_id.last_statement_id + self.assertEqual(cash_statement, session.bank_statement_id) # Cash statement should be the one linked to the session + statement_lines = cash_statement.line_ids + self.assertEqual(len(statement_lines), 1) + self.assertEqual(statement_lines.amount, 33.9) # 10 + 6% tax + 10 + 12% tax + 10 + 21% tax + + def test_closing_entry_by_product(self): + self.pos_config.use_closing_entry_by_product = True + session = self.open_pos_session() + + # Create a PoS order with 2 products with different taxes + self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 21.8}]], # Total amount of the order is 10 + 6% tax + 10 + 12% tax = 21.8 + products=[[self.product_6, {}], [self.product_12, {}]], + extra_data={'partner_id': self.partner_1.id}, + ) + + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + session.close_session_from_ui({self.cash_pm.id: expected_cashbox_amount}) + self.assertEqual(session.state, 'closed') + + sale_move = session.move_ids + self.assertEqual(len(sale_move.line_ids), 5) # 1 payment_term + 2 product + 2 tax + product_lines = sale_move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + tax_lines = sale_move.line_ids.filtered( + lambda line: line.display_type == 'tax', + ) + product1 = product_lines[0] + product2 = product_lines[1] + self.assertEqual(product1.product_id, self.product_6) # First product line should be for product with 6% tax + self.assertEqual(product2.product_id, self.product_12) # Second product line should be for product with 12% tax + self.assertEqual(product1.tax_ids.ids, [self.tax_6.id]) # First tax line should be for 6% tax + self.assertEqual(product2.tax_ids.ids, [self.tax_12.id]) # Second tax line should be for 12% tax + self.assertEqual(tax_lines[0].amount_currency, -0.6) # First tax line should be at 0.6 (6% of 10) + self.assertEqual(tax_lines[1].amount_currency, -1.2) # Second tax line should be at 1.2 (12% of 10) + + def test_separate_invoicing_pos_order(self): + session = self.open_pos_session() + + # Create a PoS order with 2 payment methods + # (customer account + cash) + pos_order = self.create_pos_order( + payment_method=[ + [self.customer_pm, {'amount': 5.3}], + [self.cash_pm, {'amount': 5.3}], + ], + products=[[self.product_6, {}]], + extra_data={'partner_id': self.partner_1.id}, + ) + + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + session.close_session_from_ui({self.cash_pm.id: expected_cashbox_amount}) + self.assertEqual(session.state, 'closed') + + # Check that the invoice is correctly created with the customer + # account payment method + invoice = self.env['account.move'].search([ + ('move_type', '=', 'out_invoice'), + ('partner_id', '=', self.partner_1.id), + ]) + self.assertEqual(pos_order.account_move, invoice) # Invoice should be linked to the PoS order + self.assertEqual(invoice.amount_total, 10.6) # 10 + 6% tax from the customer account order + self.assertEqual(invoice.amount_tax, 0.6) # 6% tax on 10 + self.assertEqual(invoice.amount_residual, 5.3) # Customer account part isn't yet paid, but cash part is paid + + # Check each account.move.line of the invoice to ensure that the + # cash payment line is reconciled with the correct invoice line + # (in case of multiple tax lines for example) + payment_terms = invoice.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + cash_payment = payment_terms[0] + customer_payment = payment_terms[1] + self.assertEqual(cash_payment.amount_currency, 5.3) # Cash part of the payment + self.assertEqual(customer_payment.amount_currency, 5.3) # Customer account part of the payment + self.assertEqual(cash_payment.reconciled, True) # Cash part + self.assertEqual(customer_payment.reconciled, False) # Customer account part + + product_line = invoice.line_ids.filtered( + lambda line: line.product_id == self.product_6, + ) + product_taxes = self.product_6.taxes_id.ids + self.assertEqual(product_line.tax_ids.ids, product_taxes) # Taxes should be correctly copied on the invoice line + self.assertEqual(product_line.amount_currency, -10.0) # Product line should be at 10 (without taxes) + self.assertEqual(product_line.credit, 10.0) # Product line should be a credit of 10 (without taxes) + + tax_lines = invoice.line_ids.filtered( + lambda line: line.display_type == 'tax', + ) + self.assertEqual(tax_lines.amount_currency, -0.6) + + def test_fixed_tax_negative_qty_should_be_negative(self): + service = self.env.ref('product.product_category_services').id + zero_amount_product = self.env['product.product'].create({ + 'name': 'Zero Amount Product', + 'available_in_pos': True, + 'list_price': 0, + 'taxes_id': [(6, 0, [self.tax_fixed.id])], + 'categ_id': service, + }) + self.pos_config.write({'iface_tax_included': 'total'}) + + # Test with a positive order first + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 1}]], + products=[[zero_amount_product, {'qty': 1}]], + ) + session.close_session_from_ui({self.cash_pm.id: 1}) + self.assertEqual(session.state, 'closed') + move = session.move_ids + tax_line = move.line_ids.filtered( + lambda line: line.display_type == 'tax', + ) + product_line = move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + payment_line = move.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + self.assertEqual(order.amount_total, 1) # Total amount should be 1 since it's a fixed tax + self.assertEqual(tax_line.credit, 1) # We credit the tax account with 1 since it's a fixed tax on a positive order + self.assertEqual(tax_line.debit, 0) # Nothing should be debited + self.assertEqual(product_line.debit, 0) # Product line should be at 0 since it's a zero amount product + self.assertEqual(product_line.credit, 0) # Product line should be at 0 since it's a zero amount product + self.assertEqual(payment_line.credit, 0) # Nothing should be credited since it's a payment + self.assertEqual(payment_line.debit, 1) # Total amount (only taxes) should be debited to the payment line since it's a positive order + + # Now test with a negative order + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': -1}]], + products=[[zero_amount_product, {'qty': -1}]], + extra_data={'is_refund': True}, + ) + session.close_session_from_ui({self.cash_pm.id: 0}) + self.assertEqual(session.state, 'closed') + move = session.move_ids + tax_line = move.line_ids.filtered( + lambda line: line.display_type == 'tax', + ) + product_line = move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + payment_line = move.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + self.assertEqual(order.amount_total, -1) # Total amount should be 1 since it's a fixed tax + self.assertEqual(tax_line.credit, 0) # Nothing should be credited since it's a refund order + self.assertEqual(tax_line.debit, 1) # We debit the tax account with 1 since it's a fixed tax on a refund + self.assertEqual(product_line.debit, 0) # Product line should be at 0 since it's a zero amount product + self.assertEqual(product_line.credit, 0) # Product line should be at 0 since it's a zero amount product + self.assertEqual(payment_line.credit, 1) # Nothing should be credited since it's a payment, but we debit the payment line with 1 since it's a refund + self.assertEqual(payment_line.debit, 0) # Total amount (only taxes) should be debited to the payment line since it's a positive order + + def test_user_right_on_statement_line_for_pos_user(self): + """Test cash difference *loss* at closing. + """ + session = self.open_pos_session() + session.close_session_from_ui({self.cash_pm.id: 0}) + session = self.open_pos_session() + self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + ) + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + session.close_session_from_ui({self.cash_pm.id: expected_cashbox_amount}) + self.assertEqual(session.state, 'closed') + bank_statement = session.bank_statement_id + self.assertEqual(bank_statement.balance_end_real, 10.6) # The order is 10 + 0.60 taxes + + def test_rounding_when_closing_session(self): + rounding_method = self.rounding_method + rounding_method.rounding_method = 'HALF-UP' + self.product_a.write({ + 'name': 'Product Test', + 'list_price': 0.04, + 'taxes_id': False, + }) + self.pos_config.write({ + 'rounding_method': rounding_method.id, + 'cash_rounding': True, + 'only_round_cash_method': False, + }) + + def check_difference(session, difference): + currency = self.pos_config.currency_id + rounded = currency.round(difference) + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + session.close_session_from_ui({self.cash_pm.id: expected_cashbox_amount}) + self.assertEqual(session.state, 'closed') + rounding_lines = session.sales_move_id.line_ids.filtered( + lambda line: line.display_type == 'rounding', + ) + if difference > 0: + self.assertEqual(rounding_lines.credit, rounded) + self.assertEqual(rounding_lines.debit, 0) + else: + self.assertEqual(rounding_lines.debit, abs(rounded)) + self.assertEqual(rounding_lines.credit, 0) + + def create_order_and_check(amount, qty=1, pm=self.cash_pm): + order = self.create_pos_order( + payment_method=[[pm, {'amount': amount}]], + products=[[self.product_a, {'qty': qty}]], + ) + rounded = rounding_method.round(order.amount_total) + self.assertEqual(order.amount_paid, rounded) + self.assertEqual(order.amount_return, 0) + return order.amount_difference + + # Only cash check when rounding amount is negative, it should be + # debited from the move to be credited on the rounding account + self.product_a.list_price = 0.04 + session = self.open_pos_session() + difference = 0 + difference += create_order_and_check(0.05) # Rounding +0.01 (total is 0.04) + difference += create_order_and_check(0.10, 2) # Rounding +0.02 (total is 0.08) + difference += create_order_and_check(0.10, 3) # Rounding -0.02 (total is 0.12) + check_difference(session, difference) # Difference +0.01 + + # Only cash check when rounding amount is positive, it should be + # credited from the move to be debited on the rounding account + self.product_a.list_price = 0.06 + session = self.open_pos_session() + difference = 0 + difference += create_order_and_check(0.05) # Rounding -0.01 (total is 0.04) + difference += create_order_and_check(0.10, 2) # Rounding -0.02 (total is 0.12) + check_difference(session, difference) # Difference -0.03 + + # Round bank payment method as well + self.product_a.list_price = 0.03 + session = self.open_pos_session() + difference = 0 + difference += create_order_and_check(0.05, 2, self.bank_pm) # Rounding -0.01 (total is 0.06) + difference += create_order_and_check(0.10, 3, self.bank_pm) # Rounding +0.01 (total is 0.09) + check_difference(session, difference) # Difference 0.00 + + # Try to round with mixed payment methods, both should be taken + # into account for the rounding + self.product_a.list_price = 0.03 + session = self.open_pos_session() + difference = 0 + difference += create_order_and_check(0.05, 2, self.bank_pm) # Rounding -0.01 (total is 0.06) + difference += create_order_and_check(0.10, 4) # Rounding -0.02 (total is 0.12) + check_difference(session, difference) # Difference -0.03 + + def test_journal_entries_category_without_account(self): + # Set company's default accounts to false + self.env.company.income_account_id = False + self.env.company.expense_account_id = False + self.product_12.write({ + '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.open_pos_session() + self.pos_config.journal_id.default_account_id = account.id + self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 11.2}]], + products=[[self.product_12, {}]], + ) + + def test_invoice_a_negative_order_should_create_credit_note(self): + self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': -10.6}]], + products=[[self.product_6, {'qty': -1}]], + extra_data={ + 'is_refund': True, + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + self.assertEqual(order.account_move.move_type, 'out_refund') # Negative order should be flagged as a refund order + + def test_order_with_positive_and_negative_lines(self): + self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10.0}]], # 10.6 * 2 + -11.2 = = 10.0 + products=[ + [self.product_6, {'qty': 2}], + [self.product_12, {'qty': -1}], + ], + extra_data={ + 'partner_id': self.partner_1.id, + }, + ) + self.close_session() + invoice = order._generate_pos_order_invoice() + self.assertEqual(order.amount_total, 10.0) + self.assertEqual(invoice.amount_total, 10.0) # Total should be 10 since we have 2 lines at 10.6 and one line at -11.2 + self.assertEqual(invoice.move_type, 'out_invoice') # Invoice should be flagged as a regular invoice + self.open_pos_session() + refund = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': -10.0}]], + products=[ + [self.product_6, {'qty': -2}], + [self.product_12, {'qty': 1}], + ], + extra_data={ + 'partner_id': self.partner_1.id, + 'is_refund': True, + }, + ) + self.close_session() + refund_invoice = refund._generate_pos_order_invoice() + self.assertEqual(refund.amount_total, -10.0) + self.assertEqual(refund_invoice.amount_total, 10.0) + self.assertEqual(refund_invoice.move_type, 'out_refund') # Refund invoice should be flagged as a refund + + def test_invoice_an_order_from_closed_session(self): + self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + extra_data={'partner_id': self.partner_1.id}, + ) + refund = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': -10.6}]], + products=[[self.product_6, {'qty': -1}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'is_refund': True, + 'refunded_order_id': order.id, + }, + ) + session = self.close_session() + + # Refund the sale order to check if move are correctly created + data = order.action_pos_order_invoice() + order_move = self.env['account.move'].browse(data['res_id']) + data = refund.action_pos_order_invoice() + refund_move = self.env['account.move'].browse(data['res_id']) + + session_sales = session.sales_move_id + session_refunds = session.refunds_move_id + reversal_sale_move = session_sales.reversal_move_ids + reversal_refund_move = session_refunds.reversal_move_ids + + # Check that the reversal moves have the exact opposite lines + # than the original moves + for sline in session_sales.line_ids: + rline = reversal_sale_move.line_ids.filtered( + lambda line: line.account_id == sline.account_id, + ) + self.assertEqual(sline.debit, rline.credit) + self.assertEqual(sline.credit, rline.debit) + + for rline in session_refunds.line_ids: + rliner = reversal_refund_move.line_ids.filtered( + lambda line: line.account_id == rline.account_id, + ) + self.assertEqual(rline.debit, rliner.credit) + self.assertEqual(rline.credit, rliner.debit) + + # Check if the account transfer exists between pos receivable + # and the customer receivable for both sale and refund + sale_account_transfer = self.env['account.move'].search([ + ('ref', 'ilike', f"invoice {order_move.name}"), + ]) + refund_account_transfer = self.env['account.move'].search([ + ('ref', 'ilike', f"invoice {refund_move.name}"), + ]) + defaul_partner = session.config_id.default_partner_id + s_receivable = defaul_partner.property_account_receivable_id + p_receivable = self.partner_1.property_account_receivable_id + + line_session = sale_account_transfer.line_ids.filtered( + lambda line: line.account_id == s_receivable, + ) + line_partner = sale_account_transfer.line_ids.filtered( + lambda line: line.account_id == p_receivable, + ) + self.assertEqual(line_session.debit, order.amount_total) + self.assertEqual(line_session.credit, 0) + self.assertEqual(line_partner.debit, 0) + self.assertEqual(line_partner.credit, order.amount_total) + + line_session = refund_account_transfer.line_ids.filtered( + lambda line: line.account_id == s_receivable, + ) + line_partner = refund_account_transfer.line_ids.filtered( + lambda line: line.account_id == p_receivable, + ) + self.assertEqual(line_session.debit, 0) + self.assertEqual(line_session.credit, abs(order.amount_total)) + self.assertEqual(line_partner.debit, abs(order.amount_total)) + self.assertEqual(line_partner.credit, 0) + + def test_order_partial_refund_rounding(self): + """ + This test ensures that the refund amount of a partial order + corresponds to the price of the item, without rounding. + """ + self.rounding_method.rounding = 5.0 + self.rounding_method.rounding_method = 'DOWN' + self.pos_config.write({ + 'rounding_method': self.rounding_method.id, + 'cash_rounding': True, + }) + + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 30}]], + products=[[self.product_6, {'qty': 3}]], + extra_data={'partner_id': self.partner_1.id}, + ) + rounded_amount = order._get_rounded_amount(order.amount_total) + self.assertEqual(rounded_amount, order.amount_paid) + + refund_action = order.refund() + refund = self.env['pos.order'].browse(refund_action['res_id']) + with Form(refund) as refund_form: + with refund_form.lines.edit(0) as line: + line.qty = -1 + refund = refund_form.save() + + self.assertEqual(refund.amount_total, -10.6) + payment_context = self.env['pos.make.payment'].with_context({ + "active_ids": refund.ids, + "active_id": refund.id, + }) + refund_payment = payment_context.create({ + 'amount': refund.amount_total, + 'payment_method_id': self.cash_pm.id, + }) + refund_payment.check() + self.assertEqual(refund.amount_paid, -10) + + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + session.close_session_from_ui({self.cash_pm.id: expected_cashbox_amount}) + + self.assertEqual(refund.state, 'done') + self.assertEqual(session.state, 'closed') + + def test_pos_order_sale_and_refund_with_taxes_not_invoiced(self): + """ + It will also check if cash_pm receivable account is correctly + updated. Statement line should also be created with the correct + amount. + """ + config_partner = self.pos_config.default_partner_id + cash_receivable = config_partner.property_account_receivable_id + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 21.8}]], # Total amount of the order is 10 + 6% tax + 10 + 12% tax = 21.8 + products=[[self.product_6, {}], [self.product_12, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + }, + ) + self.assertEqual(self.product_6.qty_available, 99) # Check that the stock of the product is correctly updated when the order is done + self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': -21.8}]], # Total amount of the order is 10 + 6% tax + 10 + 12% tax = 21.8 + products=[ + [self.product_6, {'qty': -1}], + [self.product_12, {'qty': -1}], + ], + extra_data={ + 'partner_id': self.partner_1.id, + 'is_refund': True, + 'refunded_order_id': order.id, + }, + ) + self.assertEqual(self.product_6.qty_available, 100) # Check that the stock of the product is correctly updated when the order is done + + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['payment_amount'] + session.close_session_from_ui({self.cash_pm.id: expected_cashbox_amount}) + self.assertEqual(session.state, 'closed') + sale_move = session.sales_move_id + refund_move = session.refunds_move_id + + self.assertEqual(sale_move.move_type, 'out_invoice') + self.assertEqual(sale_move.amount_total, 21.8) + self.assertEqual(refund_move.move_type, 'out_refund') + self.assertEqual(refund_move.amount_total, 21.8) + + sale_product_lines = sale_move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + refund_product_lines = refund_move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + self.assertEqual(len(sale_product_lines), 2) + self.assertEqual(len(refund_product_lines), 2) + taxes = [self.tax_6, self.tax_12] + zippeeeed = zip(sale_product_lines, refund_product_lines, taxes) + for sale_line, refund_line, tax in zippeeeed: + self.assertEqual(sale_line.tax_ids, refund_line.tax_ids) + self.assertEqual(sale_line.tax_ids.ids, tax.ids) + self.assertEqual(sale_line.credit, refund_line.debit) + + cash_move = self.env['account.move.line'].search([ + ('account_id', '=', cash_receivable.id), + ]).mapped('amount_currency') + self.assertEqual([-21.8, 21.8, 21.8, -21.8], cash_move) # The config default journal is used because cash_pm doesn't have a journal + + product_line_balance = sale_move.line_ids.filtered( + lambda line: line.display_type == 'product', + ).mapped('balance') + self.assertEqual([-10.0, -10.0], product_line_balance) # Product lines should be at 10 (without taxes) + + tax_line_balance = sale_move.line_ids.filtered( + lambda line: line.display_type == 'tax', + ).mapped('balance') + self.assertEqual([-0.6, -1.2], tax_line_balance) # Tax lines should be at 0.6 and 1.2 for the 6 + + sale_move = session.move_ids + cash_statement = self.cash_pm.journal_id.last_statement_id + self.assertEqual(cash_statement, session.bank_statement_id) # Cash statement should be the one linked to the session + cash_lines = cash_statement.line_ids.mapped('amount') + self.assertEqual([21.8, -21.8], cash_lines) # One by closing entry (refund and sale) + + def test_pos_order_sale_and_refund_with_taxes_invoiced(self): + """ + This test will check if the taxes on refunds and sales are + correctly applied when closing the session or when invoicing + the order. + + It will also check if cash_pm receivable account is correctly + updated. Statement line should also be created with the correct + amount. + """ + cash_receivable = self.partner_1.property_account_receivable_id + session = self.open_pos_session() + + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 21.8}]], # Total amount of the order is 10 + 6% tax + 10 + 12% tax = 21.8 + products=[[self.product_6, {}], [self.product_12, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + self.assertEqual(self.product_6.qty_available, 99) # Check that the stock of the product is correctly updated when the order is done + refund = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': -21.8}]], # Total amount of the order is 10 + 6% tax + 10 + 12% tax = 21.8 + products=[ + [self.product_6, {'qty': -1}], + [self.product_12, {'qty': -1}], + ], + extra_data={ + 'partner_id': self.partner_1.id, + 'is_refund': True, + 'refunded_order_id': order.id, + 'to_invoice': True, + }, + ) + self.assertEqual(self.product_6.qty_available, 100) # Check that the stock of the product is correctly updated when the order is done + sale_move = order.account_move + refund_move = refund.account_move + + self.assertEqual(sale_move.move_type, 'out_invoice') + self.assertEqual(sale_move.amount_total, 21.8) + self.assertEqual(refund_move.move_type, 'out_refund') + self.assertEqual(refund_move.amount_total, 21.8) + + sale_product_lines = sale_move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + refund_product_lines = refund_move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + self.assertEqual(len(sale_product_lines), 2) + self.assertEqual(len(refund_product_lines), 2) + taxes = [self.tax_6, self.tax_12] + zippeeeed = zip(sale_product_lines, refund_product_lines, taxes) + for sale_line, refund_line, tax in zippeeeed: + self.assertEqual(sale_line.tax_ids, refund_line.tax_ids) + self.assertEqual(sale_line.tax_ids.ids, tax.ids) + self.assertEqual(sale_line.credit, refund_line.debit) + + cash_move = self.env['account.move.line'].search([ + ('account_id', '=', cash_receivable.id), + ]).mapped('amount_currency') + self.assertEqual([-21.8, 21.8, 21.8, -21.8], cash_move) # The config default journal is used because cash_pm doesn't have a journal + + product_line_balance = sale_move.line_ids.filtered( + lambda line: line.display_type == 'product', + ).mapped('balance') + self.assertEqual([-10.0, -10.0], product_line_balance) # Product lines should be at 10 (without taxes) + + tax_line_balance = sale_move.line_ids.filtered( + lambda line: line.display_type == 'tax', + ).mapped('balance') + self.assertEqual([-0.6, -1.2], tax_line_balance) # Tax lines should be at 0.6 and 1.2 for the 6 + + cash_statement = self.cash_pm.journal_id.last_statement_id + self.assertEqual(cash_statement, session.bank_statement_id) # Cash statement should be the one linked to the session + cash_lines = cash_statement.line_ids.mapped('amount') + self.assertEqual([21.8, -21.8], cash_lines) # One by invoice + + def test_invoiced_order_with_discount_sale_and_refund_with_tax(self): + """ + This test will check if the taxes on refunds and sales with + discount are correctly applied when closing the session or when + invoicing the order. + + It will also check if cash_pm receivable account is correctly + updated. Statement line should also be created with the correct + amount. + """ + cash_receivable = self.partner_1.property_account_receivable_id + self.open_pos_session() + + order = self.create_pos_order( + products=[ + [self.product_6, {'discount': 50}], + [self.product_12, {'discount': 50}], + ], + payment_method=[[self.cash_pm, {'amount': 10.9}]], # Total amount of the order is 5 + 6% tax + 5 + 12% tax = 10.9 + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + refund = self.create_pos_order( + products=[ + [self.product_6, {'discount': 50, 'qty': -1}], + [self.product_12, {'discount': 50, 'qty': -1}], + ], + payment_method=[[self.cash_pm, {'amount': -10.9}]], # Total amount of the order is 5 + 6% tax + 5 + 12% tax = 10.9 + extra_data={ + 'partner_id': self.partner_1.id, + 'is_refund': True, + 'refunded_order_id': order.id, + 'to_invoice': True, + }, + ) + sale_move = order.account_move + refund_move = refund.account_move + + refund_product_line = refund_move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + sale_product_line = sale_move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + zippeeeed = zip(sale_product_line, refund_product_line) + for sale_line, refund_line in zippeeeed: + self.assertEqual(sale_line.discount, 50) # Discount should be at 50% + self.assertEqual(sale_line.discount, refund_line.discount) # Discount should be the same on sale and refund + + self.assertEqual(sale_move.move_type, 'out_invoice') + self.assertEqual(sale_move.amount_total, 10.9) + self.assertEqual(refund_move.move_type, 'out_refund') + self.assertEqual(refund_move.amount_total, 10.9) + + cash_move = self.env['account.move.line'].search([ + ('account_id', '=', cash_receivable.id), + ]).mapped('amount_currency') + self.assertEqual([-10.9, 10.9, 10.9, -10.9], cash_move) + + def test_pos_payment_direction_and_account(self): + """ + Ensure POS payments create correct inbound/outbound payments + and accounts. This test will check for invoiced orders and + classic global entry of the session. + """ + inbound = self.inbound_payment_method_line.payment_account_id + self.bank_pm.outstanding_account_id = inbound.id + config_partner = self.pos_config.default_partner_id + pos_receivable = config_partner.property_account_receivable_id + session = self.env['pos.session'] + + def create_session_with_single_order(**kwargs): + config = self.pos_config + config.open_ui() + session = self.get_pos_session() + cash_acc = config._get_opening_balance() + session.set_opening_control(cash_acc, "") + order = self.create_pos_order(**kwargs) + self.assertEqual(order.state, 'paid') + closing_data = session.get_closing_control_data() + cash_details = closing_data['default_cash_details'] + expected_cashbox_amount = cash_details['amount'] + session.close_session_from_ui({self.cash_pm.id: expected_cashbox_amount}) + self.assertEqual(session.state, 'closed') + return session + + session |= create_session_with_single_order( + payment_method=[[self.bank_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + ) + session |= create_session_with_single_order( + payment_method=[[self.bank_pm, {'amount': -10.6}]], + products=[[self.product_6, {'qty': -1}]], + extra_data={'is_refund': True}, + ) + session |= create_session_with_single_order( + payment_method=[[self.cash_pm, {'amount': 11.2}]], + products=[[self.product_12, {}]], + ) + session |= create_session_with_single_order( + payment_method=[[self.cash_pm, {'amount': -11.2}]], + products=[[self.product_12, {'qty': -1}]], + extra_data={'is_refund': True}, + ) + payments = self.env['account.payment'].search( + [('pos_session_id', 'in', session.ids)], + order='id', + ) + + # Check bank payments in global entry + expected_out_acc = self.bank_pm.outstanding_account_id + destination_account = payments.destination_account_id + outstanding_account = payments.outstanding_account_id + self.assertEqual(len(payments), 2) + self.assertEqual(destination_account, pos_receivable) # Bank payment should be posted on the account defined on the payment method or default config account + self.assertEqual(outstanding_account, expected_out_acc) + self.assertEqual(payments.pos_payment_method_id, self.bank_pm) # Only bank pm cash payment is managed from statements + self.assertEqual( + payments.mapped('payment_type'), + ['inbound', 'outbound'], # One sale and one refund + ) + + # Check cash payments + lines = session.bank_statement_id.line_ids + self.assertEqual(len(lines), 2) + self.assertEqual(lines.mapped('amount'), [11.2, -11.2]) # One line for the sale and one for the refund + accounts = lines.invoice_line_ids.account_id.ids + cash_acc = self.cash_pm.journal_id.default_account_id + self.assertEqual(accounts, [cash_acc.id, pos_receivable.id]) # Cash payment should be posted on the cash account of the statement + + # Open session + self.pos_config.open_ui() + session = self.get_pos_session() + cash_acc = self.pos_config._get_opening_balance() + session.set_opening_control(cash_acc, "") + order1 = self.create_pos_order( + payment_method=[[self.bank_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + refund1 = self.create_pos_order( + payment_method=[[self.bank_pm, {'amount': -10.6}]], + products=[[self.product_6, {'qty': -1}]], + extra_data={ + 'is_refund': True, + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + order2 = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 11.2}]], + products=[[self.product_12, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + refund2 = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': -11.2}]], + products=[[self.product_12, {'qty': -1}]], + extra_data={ + 'is_refund': True, + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + + # Check bank payments for invoiced orders + expected_out_acc = self.bank_pm.outstanding_account_id + destination_account = payments.destination_account_id + outstanding_account = payments.outstanding_account_id + self.assertEqual(len(payments), 2) + self.assertEqual(destination_account, pos_receivable) # Bank payment should be posted on the account defined on the payment method or default config account + self.assertEqual(outstanding_account, expected_out_acc) + self.assertEqual(payments.pos_payment_method_id, self.bank_pm) # Only bank pm cash payment is managed from statements + self.assertEqual( + payments.mapped('payment_type'), + ['inbound', 'outbound'], # One sale and one refund + ) + + sale_invoices = order1.account_move + order2.account_move + refund_invoices = refund1.account_move + refund2.account_move + partner_invoices = self.partner_1.invoice_ids.ids + for id in sale_invoices.ids + refund_invoices.ids: + self.assertIn(id, partner_invoices) + + def test_invoicing_zero_amount_pos_order(self): + session = self.open_pos_session() + self.product_6.lst_price = 0 + + order = self.create_pos_order( + payment_method=[ + [self.cash_pm, {'amount': 0}], + ], + products=[[self.product_6, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + + session.close_session_from_ui({self.cash_pm.id: 0}) + self.assertEqual(session.state, 'closed') + + receivable = self.partner_1.property_account_receivable_id + line_ids = order.account_move.line_ids + self.assertEqual(len(line_ids), 2) # Only one line for the product + self.assertEqual(line_ids[0].account_id, receivable) # Line should be posted on the partner receivable + self.assertEqual(line_ids[0].display_type, 'payment_term') + self.assertEqual(line_ids[0].debit, 0) # Line should be at 0 since it's a zero amount order + self.assertEqual(line_ids[0].credit, 0) # Line should be at 0 since it's a zero amount order + + # Check product line + self.assertEqual(line_ids[1].display_type, 'product') + self.assertEqual(line_ids[1].debit, 0) # Line should be at 0 since it's a zero amount order + self.assertEqual(line_ids[1].credit, 0) # Line should be at 0 since it's a zero amount order + + def test_various_orders(self): + StatementLine = self.env['account.bank.statement.line'] + BankPayment = self.env['account.payment'] + defaut_partner = self.pos_config.default_partner_id + + def create_order_and_check(order_args, values={ + 'amount_total': 0, + 'amount_tax': 0, + 'amount_paid': 0, + }): + receivable = defaut_partner.property_account_receivable_id + cash_acc = self.pos_config._get_opening_balance() + session = self.open_pos_session(cash_acc) + order = self.create_pos_order(**order_args) + + self.assertEqual(order.state, 'paid') + self.assertEqual(order.amount_total, values['amount_total']) + self.assertEqual(order.amount_tax, values['amount_tax']) + self.assertEqual(order.amount_paid, values['amount_paid']) + self.assertEqual(order.state, 'paid') + self.assertEqual(session.state, 'opened') + + cash_pm = order.payment_ids.filtered( + lambda pm: pm.payment_method_id.type == 'cash', + ) + closing_amount = sum(cash_pm.mapped('amount')) + cash_acc + session.close_session_from_ui({self.cash_pm.id: closing_amount}) + move = session.move_ids + + self.assertEqual(session.state, 'closed') + self.assertEqual(order.state, 'done') + + if order.is_singly_invoiced and order.account_move: + move = order.account_move + partner = order.partner_id + nb_product = len(order_args['products']) + nb_payment = len(order_args['payment_method']) + tax_ids = [p[0].taxes_id.ids for p in order_args['products']] + flat = [x for xs in tax_ids for x in xs] + nb_lines = nb_product + nb_payment + len(flat) + self.assertEqual(move.state, 'posted') + self.assertEqual(len(move.line_ids), nb_lines) + self.assertEqual(move.amount_total, order.amount_total) + self.assertEqual(move.amount_tax, order.amount_tax) + + if partner.property_account_receivable_id: + receivable = partner.property_account_receivable_id + + payment_term = move.line_ids.filtered( + lambda line: line.display_type == 'payment_term', + ) + zipped = zip(order.payment_ids, payment_term) + for payment, term in zipped: + pm = payment.payment_method_id + + if not order.to_invoice and pm.receivable_account_id: + receivable = pm.receivable_account_id + + if pm.type == 'cash': + self.assertEqual(term.account_id, receivable) + statement = StatementLine.search([ + ('pos_session_id', '=', order.session_id.id), + ]) + self.assertEqual(len(statement), 1) + self.assertEqual(statement.amount, payment.amount) + elif pm.type == 'pay_later': + session_move = session.sales_move_id + self.assertEqual(order.to_invoice, True) + self.assertNotEqual( + order.account_move, + session_move, + ) + acc = self.partner_1.property_account_receivable_id + self.assertEqual(term.account_id, acc) + elif pm.type == 'bank': + payment = BankPayment.search([ + ('pos_session_id', '=', order.session_id.id), + ]) + self.assertEqual(term.account_id, receivable) + self.assertEqual(len(payment), 1) + self.assertEqual(payment.amount, term.debit) + + one_product_6_check_values = { + 'amount_total': 10.6, + 'amount_tax': 0.6, + 'amount_paid': 10.6, + } + create_order_and_check( + order_args={ + 'payment_method': [[self.customer_pm, {'amount': 10.6}]], + 'products': [[self.product_6, {}]], + 'extra_data': {'partner_id': self.partner_1.id}, + }, + values=one_product_6_check_values, + ) + create_order_and_check( + order_args={ + 'payment_method': [[self.bank_pm, {'amount': 10.6}]], + 'products': [[self.product_6, {}]], + }, + values=one_product_6_check_values, + ) + create_order_and_check( + order_args={ + 'payment_method': [[self.cash_pm, {'amount': 10.6}]], + 'products': [[self.product_6, {}]], + }, + values=one_product_6_check_values, + ) + create_order_and_check( + order_args={ + 'payment_method': [ + [self.cash_pm, {'amount': 5.3}], + [self.bank_pm, {'amount': 5.3}], + ], + 'products': [[self.product_6, {}]], + }, + values=one_product_6_check_values, + ) + create_order_and_check( + order_args={ + 'payment_method': [ + [self.bank_pm, {'amount': 2.3}], + [self.customer_pm, {'amount': 5.3}], + [self.cash_pm, {'amount': 3.0}], + ], + 'products': [[self.product_6, {}]], + 'extra_data': {'partner_id': self.partner_1.id}, + }, + values=one_product_6_check_values, + ) + total = 10.6 + 11.2 + 12.1 + create_order_and_check( + order_args={ + 'payment_method': [[self.bank_pm, {'amount': total}]], + 'products': [ + [self.product_6, {}], + [self.product_12, {}], + [self.product_21, {}], + ], + }, + values={ + 'amount_total': total, + 'amount_paid': total, + 'amount_tax': 0.6 + 1.2 + 2.1, + }, + ) + total = 13.9 + create_order_and_check( + order_args={ + 'payment_method': [[self.bank_pm, {'amount': total}]], + 'products': [ + [self.product_6_12_21, {}], + ], + }, + values={ + 'amount_total': total, + 'amount_paid': total, + 'amount_tax': 0.6 + 1.2 + 2.1, + }, + ) + self.tax_6.price_include_override = 'tax_included' + create_order_and_check( + order_args={ + 'payment_method': [[self.bank_pm, {'amount': 10}]], + 'products': [[self.product_6, {}]], + }, + values={ + 'amount_total': 10, + 'amount_paid': 10, + 'amount_tax': 0.57, + }, + ) + + def test_pos_config_with_other_currency_than_company(self): + eur = self.env.ref('base.EUR') + usd = self.env.ref('base.USD') + self.env['res.currency.rate'].search([]).unlink() + self.env['res.currency.rate'].create({ + 'name': '2010-01-01', + 'rate': 2.0, + 'currency_id': usd.id, + }) + + self.pos_config.company_id.currency_id = eur + self.pos_config.journal_id.currency_id = usd + self.pos_config.currency_id = usd + self.cash_pm.journal_id.currency_id = usd + + self.assertEqual(self.pos_config.company_id.currency_id, eur) + self.assertEqual(self.pos_config.currency_id, usd) + self.assertEqual(self.pos_config.journal_id.currency_id, usd) + self.assertEqual(self.cash_pm.journal_id.currency_id, usd) + + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 21.8}]], # Total amount of the order is 10 + 6% tax + 10 + 12% tax = 21.8 + products=[[self.product_6, {}], [self.product_12, {}]], + ) + + self.assertEqual(order.amount_total, 21.8) + self.assertEqual(order.amount_paid, 21.8) + self.assertEqual(order.state, 'paid') + self.close_session() + + self.assertTrue(session.move_ids) + self.assertEqual(session.move_ids.currency_id, usd) + + total_usd = session.move_ids.amount_total_in_currency_signed + total_eur = session.move_ids.amount_total_signed + self.assertEqual(total_usd, 21.8) + self.assertEqual(total_eur, 10.9) # 21.8 / 2 because of the rate we set on USD + + session = self.open_pos_session() + invoiced_order = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 21.8}]], # Total amount of the order is 10 + 6% tax + 10 + 12% tax = 21.8 + products=[[self.product_6, {}], [self.product_12, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + move = invoiced_order.account_move + self.assertEqual(move.currency_id, usd) + total_usd = move.amount_total_in_currency_signed + total_eur = move.amount_total_signed + self.assertEqual(total_usd, 21.8) + self.assertEqual(total_eur, 10.9) # 21.8 / 2 because of the rate we set on USD + + def test_accounting_items_when_closing_with_bank_difference(self): + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.bank_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + ) + self.assertEqual(order.state, 'paid') + session.close_session_from_ui({self.bank_pm.id: 9.6}) # Simulate a bank difference of -1 + self.assertEqual(session.state, 'closed') + + move = session.correction_move_ids + account_names = move.line_ids.mapped('account_id.name') + balances = move.line_ids.mapped('balance') + + self.assertEqual(balances, [-1.0, 1.0]) + self.assertEqual( + account_names, + ['Accounts Receivable (PoS)', 'Cash Difference Loss'], + ) + + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.bank_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + ) + self.assertEqual(order.state, 'paid') + session.close_session_from_ui({self.bank_pm.id: 11.6}) # Simulate a bank difference of +1 + self.assertEqual(session.state, 'closed') + + move = session.correction_move_ids + account_names = move.line_ids.mapped('account_id.name') + balances = move.line_ids.mapped('balance') + + self.assertEqual(balances, [1.0, -1.0]) + self.assertEqual( + account_names, + ['Accounts Receivable (PoS)', 'Cash Difference Gain'], + ) + + def test_pos_order_with_closing_storno(self): + with freeze_time('2020-01-01'): + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.bank_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + ) + self.create_pos_order( + payment_method=[[self.bank_pm, {'amount': -10.6}]], + products=[[self.product_6, {'qty': -1}]], + extra_data={ + 'is_refund': True, + 'refunded_order_id': order.id, + }, + ) + self.close_session() + classic_refund = session.refunds_move_id + classic_sale = session.sales_move_id + + self.env.company.account_storno = True + session = self.open_pos_session() + order = self.create_pos_order( + payment_method=[[self.bank_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + ) + self.create_pos_order( + payment_method=[[self.bank_pm, {'amount': -10.6}]], + products=[[self.product_6, {'qty': -1}]], + extra_data={ + 'is_refund': True, + 'refunded_order_id': order.id, + }, + ) + self.close_session() + storno_refund = session.refunds_move_id + storno_sale = session.sales_move_id + + r_classic_credit = classic_refund.line_ids.mapped('credit') + r_classic_debit = classic_refund.line_ids.mapped('debit') + r_storno_credit = storno_refund.line_ids.mapped('credit') + r_storno_debit = storno_refund.line_ids.mapped('debit') + + self.assertEqual(r_classic_credit, [10.6, 0.0, 0.0]) + self.assertEqual(r_classic_debit, [0.0, 10.0, 0.6]) + self.assertEqual(r_storno_credit, [0.0, -10.0, -0.6]) + self.assertEqual(r_storno_debit, [-10.6, 0.0, 0.0]) + + s_classic_credit = classic_sale.line_ids.mapped('credit') + s_classic_debit = classic_sale.line_ids.mapped('debit') + s_storno_credit = storno_sale.line_ids.mapped('credit') + s_storno_debit = storno_sale.line_ids.mapped('debit') + + self.assertEqual(s_classic_credit, s_storno_credit) + self.assertEqual(s_classic_debit, s_storno_debit) + + def test_pos_order_session_closing_with_fp(self): + """ + PoS orders can have fiscal positions applied on them. This test + will check that when closing a session with orders with fiscal + position, the correct taxes are applied and the correct accounts + are used for the move lines of the session move. + + Will test to use the tax 21% with a fiscal position that + replaces it with the tax 6%. + + Will also test with a product with tax of 12% with no + destination tax (0%) + """ + src_account = self.company.income_account_id + dest_account = self.env['account.account'].search([ + ('company_ids', '=', self.company.id), + ('account_type', '=', 'income'), + ('id', '!=', src_account.id), + ], limit=1) + + fp = self.env['account.fiscal.position'].create({ + 'name': 'Test Fiscal Position', + }) + account_fp = self.env['account.fiscal.position.account'].create({ + 'position_id': fp.id, + 'account_src_id': src_account.id, + 'account_dest_id': dest_account.id, + }) + fp.write({ + 'account_ids': [(6, 0, account_fp.ids)], + }) + self.tax_6.write({ + 'fiscal_position_ids': [Command.link(fp.id)], + 'original_tax_ids': [Command.link(self.tax_21.id)], + }) + self.env['account.tax'].create({ + 'name': 'Tax 0%', + 'amount': 0, + 'fiscal_position_ids': [Command.link(fp.id)], + 'original_tax_ids': [Command.link(self.tax_12.id)], + }) + + # So when selling a product with 21% tax, the fiscal position + # should replace it with the 6% tax + session = self.open_pos_session() + order_with_fp = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10.6}]], # Fiscal position apply 6% tax instead of 21% tax + products=[[self.product_21, {}]], + extra_data={ + 'fiscal_position_id': fp.id, + }, + ) + self.assertEqual(order_with_fp.amount_total, 10.6) + self.assertEqual(order_with_fp.amount_tax, 0.6) + self.assertEqual(order_with_fp.amount_paid, 10.6) + self.assertEqual(order_with_fp.state, 'paid') + + order_without_fp = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 12.1}]], # No fiscal position, so 21% tax should be applied + products=[[self.product_21, {}]], + ) + self.assertEqual(order_without_fp.amount_total, 12.1) + self.assertEqual(order_without_fp.amount_tax, 2.1) + self.assertEqual(order_without_fp.amount_paid, 12.1) + self.assertEqual(order_without_fp.state, 'paid') + + order_with_0_tax = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10}]], # Fiscal position applies 0% tax instead of 12% tax + products=[[self.product_12, {}]], + extra_data={ + 'fiscal_position_id': fp.id, + }, + ) + self.assertEqual(order_with_0_tax.amount_total, 10) + self.assertEqual(order_with_0_tax.amount_tax, 0) + self.assertEqual(order_with_0_tax.amount_paid, 10) + self.assertEqual(order_with_0_tax.state, 'paid') + self.close_session() + move = session.move_ids + product_line = move.line_ids.filtered( + lambda line: line.display_type == 'product', + ) + tax_lines = move.line_ids.filtered( + lambda line: line.display_type == 'tax', + ) + + tax_sorted = tax_lines.sorted('balance') + self.assertEqual(tax_sorted[0].balance, -2.1) + self.assertEqual(tax_sorted[1].balance, -0.6) + + product_line_6 = product_line.filtered( + lambda line: self.tax_6 in line.tax_ids, + ) + product_line_21 = product_line.filtered( + lambda line: self.tax_21 in line.tax_ids, + ) + product_line_0 = product_line - product_line_6 - product_line_21 + self.assertEqual(product_line_6.account_id, dest_account) # Product line with tax_6 should be posted on the dest account of the fiscal position + self.assertEqual(product_line_21.account_id, src_account) # Product line with tax_21 should be posted on the src account of the fiscal position + self.assertEqual(product_line_0.account_id, dest_account) + + def test_pos_order_invoice_with_fp(self): + src_account = self.company.income_account_id + dest_account = self.env['account.account'].search([ + ('company_ids', '=', self.company.id), + ('account_type', '=', 'income'), + ('id', '!=', src_account.id), + ], limit=1) + + fp = self.env['account.fiscal.position'].create({ + 'name': 'Test Fiscal Position', + }) + account_fp = self.env['account.fiscal.position.account'].create({ + 'position_id': fp.id, + 'account_src_id': src_account.id, + 'account_dest_id': dest_account.id, + }) + fp.write({ + 'account_ids': [(6, 0, account_fp.ids)], + }) + self.tax_6.write({ + 'fiscal_position_ids': [Command.link(fp.id)], + 'original_tax_ids': [Command.link(self.tax_21.id)], + }) + self.env['account.tax'].create({ + 'name': 'Tax 0%', + 'amount': 0, + 'fiscal_position_ids': [Command.link(fp.id)], + 'original_tax_ids': [Command.link(self.tax_12.id)], + }) + + # So when selling a product with 21% tax, the fiscal position + # should replace it with the 6% tax + session = self.open_pos_session() + order_with_fp = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10.6}]], # Fiscal position apply 6% tax instead of 21% tax + products=[[self.product_21, {}]], + extra_data={ + 'fiscal_position_id': fp.id, + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + self.assertEqual(order_with_fp.amount_total, 10.6) + self.assertEqual(order_with_fp.amount_tax, 0.6) + self.assertEqual(order_with_fp.amount_paid, 10.6) + self.assertEqual(order_with_fp.state, 'paid') + self.assertEqual(order_with_fp.account_move.state, 'posted') + move = order_with_fp.account_move + self.assertEqual(move.fiscal_position_id, fp) + + order_without_fp = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 12.1}]], # No fiscal position, so 21% tax should be applied + products=[[self.product_21, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + self.assertEqual(order_without_fp.amount_total, 12.1) + self.assertEqual(order_without_fp.amount_tax, 2.1) + self.assertEqual(order_without_fp.amount_paid, 12.1) + self.assertEqual(order_without_fp.state, 'paid') + self.assertEqual(order_without_fp.account_move.state, 'posted') + move = order_without_fp.account_move + self.assertFalse(move.fiscal_position_id) + + order_with_0_tax = self.create_pos_order( + payment_method=[[self.cash_pm, {'amount': 10}]], # Fiscal position applies 0% tax instead of 12% tax + products=[[self.product_12, {}]], + extra_data={ + 'fiscal_position_id': fp.id, + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + self.assertEqual(order_with_0_tax.amount_total, 10) + self.assertEqual(order_with_0_tax.amount_tax, 0) + self.assertEqual(order_with_0_tax.amount_paid, 10) + self.assertEqual(order_with_0_tax.state, 'paid') + self.assertEqual(order_with_0_tax.account_move.state, 'posted') + move = order_with_0_tax.account_move + self.assertEqual(move.fiscal_position_id, fp) + self.close_session() + self.assertFalse(session.move_ids) + + def test_pos_order_with_company_branch(self): + branch = self.env['res.company'].create({ + 'name': 'Sub Company', + 'parent_id': self.env.company.id, + 'chart_template': self.env.company.chart_template, + 'country_id': self.env.company.country_id.id, + }) + self.env.cr.precommit.run() + + AccJournal = self.env['account.journal'].with_company(branch) + PosPm = self.env['pos.payment.method'].with_company(branch) + PosConfig = self.env['pos.config'].with_company(branch) + + cash_journal = AccJournal.create({ + 'name': 'Cash Test', + 'type': 'cash', + 'company_id': branch.id, + 'code': 'CSH', + 'sequence': 10, + }) + cash_pm = PosPm.create({ + 'name': 'Bank', + 'type': 'cash', + 'journal_id': cash_journal.id, + }) + self.pos_config = PosConfig.create({ + 'name': 'Main - Sub Company', + 'journal_id': self.company_data['default_journal_sale'].id, + 'payment_method_ids': [(4, cash_pm.id)], + }) + + self.open_pos_session() + order = self.create_pos_order( + payment_method=[[cash_pm, {'amount': 10.6}]], + products=[[self.product_6, {}]], + extra_data={ + 'partner_id': self.partner_1.id, + 'to_invoice': True, + }, + ) + self.close_session() + self.assertEqual(order.config_id, self.pos_config) diff --git a/addons/point_of_sale/tests/test_pos_accounting_http.py b/addons/point_of_sale/tests/test_pos_accounting_http.py new file mode 100644 index 00000000000000..7249e20d0417ff --- /dev/null +++ b/addons/point_of_sale/tests/test_pos_accounting_http.py @@ -0,0 +1,68 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import json + +from odoo.tests import HttpCase + +from odoo.addons.point_of_sale.tests.test_pos_accounting import TestPosAccounting + + +class TestPosAccountingHttp(HttpCase, TestPosAccounting): + def _get_url(self, pos_config=None): + pos_config = pos_config or self.pos_config + return f"/pos/ui/{pos_config.id}" + + def start_pos_tour(self, tour_name, **kwargs): + self.start_tour(self._get_url(pos_config=kwargs.get('pos_config')), tour_name, login=self.env.user.login, **kwargs) + + def test_baseline_between_frontend_and_backend(self): + company = self.pos_config.company_id + company.tax_calculation_rounding_method = 'round_globally' + + only_categ = self.env['pos.category'].create( + {'name': 'Only Category'}, + ) + self.pos_config.write({ + 'limit_categories': True, + 'iface_available_categ_ids': [(6, 0, [only_categ.id])], + }) + 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, + 'pos_categ_ids': [(6, 0, [only_categ.id])], + 'taxes_id': [(6, 0, [tax_16.id])], + 'available_in_pos': True, + }, { + 'name': 'Test Product 2', + 'list_price': 352.59, + 'pos_categ_ids': [(6, 0, [only_categ.id])], + 'taxes_id': [(6, 0, [tax_16.id])], + 'available_in_pos': True, + }]) + + def get_frontend_data(self, frontend_data): + frontend_data = json.loads(frontend_data) + base_lines = self.lines._prepare_base_lines_for_taxes_computation() + zipped = zip(frontend_data['baseLines'], base_lines) + for frontend_line, backend_line in zipped: + if frontend_line.get('is_refund', False) != backend_line['is_refund']: + error = "Refund status mismatch between frontend and backend" + raise ValueError(error) + + if frontend_line.get('quantity', 0) != backend_line['quantity']: + error = "Quantity mismatch between frontend and backend" + raise ValueError(error) + + if frontend_line.get('sign') != backend_line['sign']: + error = "Sign mismatch between frontend and backend" + raise ValueError(error) + + # Add function to model + order_model = self.env.registry.models['pos.order'] + order_model.get_frontend_data = get_frontend_data + + self.open_pos_session() + self.start_pos_tour('test_baseline_between_frontend_and_backend') diff --git a/addons/point_of_sale/tests/test_pos_basic_config.py b/addons/point_of_sale/tests/test_pos_basic_config.py index 0fa0d28f497e00..9ce16be6b884d5 100644 --- a/addons/point_of_sale/tests/test_pos_basic_config.py +++ b/addons/point_of_sale/tests/test_pos_basic_config.py @@ -1,15 +1,10 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import odoo -from odoo import fields from odoo.addons.point_of_sale.tests.common import TestPoSCommon from odoo.exceptions import ValidationError, UserError -from freezegun import freeze_time -from dateutil.relativedelta import relativedelta from datetime import datetime, timedelta -import unittest.mock @odoo.tests.tagged('post_install', '-at_install') @@ -32,820 +27,6 @@ def setUp(self): self.product_multi_tax = self.create_product('Multi-tax product', self.categ_basic, 100, 100, (self.taxes['tax8'] | self.taxes['tax9']).ids) self.company_data_2 = self.setup_other_company() - def test_orders_no_invoiced(self): - """ Test for orders without invoice - - 3 orders - - first 2 orders with cash payment - - last order with bank payment - - Orders - ====== - +---------+----------+-----------+----------+-----+-------+ - | order | payments | invoiced? | product | qty | total | - +---------+----------+-----------+----------+-----+-------+ - | order 1 | cash | no | product1 | 10 | 100 | - | | | | product2 | 5 | 100 | - +---------+----------+-----------+----------+-----+-------+ - | order 2 | cash | no | product2 | 7 | 140 | - | | | | product3 | 1 | 30 | - +---------+----------+-----------+----------+-----+-------+ - | order 3 | bank | no | product1 | 1 | 10 | - | | | | product2 | 3 | 60 | - | | | | product3 | 5 | 150 | - +---------+----------+-----------+----------+-----+-------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | sale | -590 | - | pos receivable cash | 370 | - | pos receivable bank | 220 | - +---------------------+---------+ - | Total balance | 0.0 | - +---------------------+---------+ - """ - start_qty_available = { - self.product1: self.product1.qty_available, - self.product2: self.product2.qty_available, - self.product3: self.product3.qty_available, - } - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - # check product qty_available after syncing the order - self.assertEqual( - self.product1.qty_available + 11, - start_qty_available[self.product1], - ) - self.assertEqual( - self.product2.qty_available + 15, - start_qty_available[self.product2], - ) - self.assertEqual( - self.product3.qty_available + 6, - start_qty_available[self.product3], - ) - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 5)], 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product2, 7), (self.product3, 1)], 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product1, 1), (self.product3, 5), (self.product2, 3)], 'payments': [(self.bank_pm1, 220)], 'uuid': '00100-010-0003'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 590, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 220, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 370, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((370, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 370, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 370, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((220, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 220, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 220, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_orders_with_invoiced(self): - """ Test for orders: one with invoice - - 3 orders - - order 1, paid by cash - - order 2, paid by bank - - order 3, paid by bank, invoiced - - Orders - ====== - +---------+----------+---------------+----------+-----+-------+ - | order | payments | invoiced? | product | qty | total | - +---------+----------+---------------+----------+-----+-------+ - | order 1 | cash | no | product1 | 6 | 60 | - | | | | product2 | 3 | 60 | - | | | | product3 | 1 | 30 | - +---------+----------+---------------+----------+-----+-------+ - | order 2 | bank | no | product1 | 1 | 10 | - | | | | product2 | 20 | 400 | - +---------+----------+---------------+----------+-----+-------+ - | order 3 | bank | yes, customer | product1 | 10 | 100 | - | | | | product3 | 1 | 30 | - +---------+----------+---------------+----------+-----+-------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | sale | -560 | - | pos receivable cash | 150 | - | pos receivable bank | 540 | - | receivable | -130 | - +---------------------+---------+ - | Total balance | 0.0 | - +---------------------+---------+ - """ - start_qty_available = { - self.product1: self.product1.qty_available, - self.product2: self.product2.qty_available, - self.product3: self.product3.qty_available, - } - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - # check product qty_available after syncing the order - self.assertEqual( - self.product1.qty_available + 17, - start_qty_available[self.product1], - ) - self.assertEqual( - self.product2.qty_available + 23, - start_qty_available[self.product2], - ) - self.assertEqual( - self.product3.qty_available + 2, - start_qty_available[self.product3], - ) - - # check account move in the invoiced order - invoiced_order = self.pos_session.order_ids.filtered(lambda order: order.account_move) - self.assertEqual(1, len(invoiced_order), 'Only one order is invoiced in this test.') - - # check account_move of orders before validating the session. - self.assertTrue(invoiced_order.account_move, msg="Invoiced orders must have account_move.") - uninvoiced_orders = self.pos_session.order_ids - invoiced_order - self.assertTrue( - all(not order.account_move for order in uninvoiced_orders), - msg="Uninvoiced orders do not have account_move." - ) - - def _after_closing_cb(): - # check state of orders after validating the session. - uninvoiced_orders = self.pos_session.order_ids.filtered(lambda order: not order.is_invoiced) - self.assertTrue( - all([order.state == 'done' for order in uninvoiced_orders]), - msg="State should be 'done' for uninvoiced orders after validating the session." - ) - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 6), (self.product2, 3), (self.product3, 1), ], 'payments': [(self.cash_pm1, 150)], 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product1, 1), (self.product2, 20), ], 'payments': [(self.bank_pm1, 410)], 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product3, 1), ], 'payments': [(self.bank_pm1, 130)], 'is_invoiced': True, 'customer': self.customer, 'uuid': '00100-010-0003'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': { - '00100-010-0003': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 30, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 130, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 130), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 130, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 130, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'after_closing_cb': _after_closing_cb, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 560, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 540, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 150, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 130, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((150, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 150, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 150, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((540, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 540, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 540, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_orders_with_zero_valued_invoiced(self): - """One invoiced order but with zero receivable line balance.""" - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product0, 1)], 'payments': [(self.bank_pm1, 0)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 0), False), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': False, - 'cash_statement': [], - 'bank_payments': [], - }, - }) - - def test_return_order_invoiced(self): - - def _before_closing_cb(): - order = self.pos_session.order_ids.filtered(lambda order: '666-666-666' in order.uuid) - - # refund - order.refund() - refund_order = self.pos_session.order_ids.filtered(lambda order: order.state == 'draft') - - # pay the refund - context_make_payment = {"active_ids": [refund_order.id], "active_id": refund_order.id} - make_payment = self.env['pos.make.payment'].with_context(context_make_payment).create({ - 'payment_method_id': self.cash_pm1.id, - 'amount': -100, - }) - make_payment.check() - - # invoice refund - refund_order.action_pos_order_invoice() - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments': [(self.cash_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '666-666-666'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': { - '666-666-666': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_pm1, 100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [], - }, - }) - - def test_return_order(self): - """ Test return order - - 2 orders - - 2nd order is returned - - Orders - ====== - +------------------+----------+-----------+----------+-----+-------+ - | order | payments | invoiced? | product | qty | total | - +------------------+----------+-----------+----------+-----+-------+ - | order 1 | bank | no | product1 | 1 | 10 | - | | | | product2 | 5 | 100 | - +------------------+----------+-----------+----------+-----+-------+ - | order 2 | cash | no | product1 | 3 | 30 | - | | | | product2 | 2 | 40 | - | | | | product3 | 1 | 30 | - +------------------+----------+-----------+----------+-----+-------+ - | order 3 (return) | cash | no | product1 | -3 | -30 | - | | | | product2 | -2 | -40 | - | | | | product3 | -1 | -30 | - +------------------+----------+-----------+----------+-----+-------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | sale (sales) | -210 | - | sale (refund) | 100 | - | pos receivable bank | 110 | - +---------------------+---------+ - | Total balance | 0.0 | - +---------------------+---------+ - """ - start_qty_available = { - self.product1: self.product1.qty_available, - self.product2: self.product2.qty_available, - self.product3: self.product3.qty_available, - } - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(2, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - # return order - order_to_return = self.pos_session.order_ids.filtered(lambda order: '12345-123-1234' in order.uuid) - order_to_return.refund() - refund_order = self.pos_session.order_ids.filtered(lambda order: order.state == 'draft') - - # check if amount to pay - self.assertAlmostEqual(refund_order.amount_total - refund_order.amount_paid, -100) - - # pay the refund - context_make_payment = {"active_ids": [refund_order.id], "active_id": refund_order.id} - make_payment = self.env['pos.make.payment'].with_context(context_make_payment).create({ - 'payment_method_id': self.cash_pm1.id, - 'amount': -100, - }) - make_payment.check() - self.assertEqual(refund_order.state, 'paid', 'Payment is registered, order should be paid.') - self.assertAlmostEqual(refund_order.amount_paid, -100.0, msg='Amount paid for return order should be negative.') - - # check product qty_available after syncing the order - self.assertEqual( - self.product1.qty_available + 1, - start_qty_available[self.product1], - ) - self.assertEqual( - self.product2.qty_available + 5, - start_qty_available[self.product2], - ) - self.assertEqual( - self.product3.qty_available, - start_qty_available[self.product3], - ) - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 1), (self.product2, 5)], 'payments': [(self.bank_pm1, 110)], 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product1, 3), (self.product2, 2), (self.product3, 1)], 'payments': [(self.cash_pm1, 100)], 'uuid': '12345-123-1234'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 210, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 110, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((110, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 110, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 110, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_split_cash_payments(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 5)], 'payments': [(self.cash_split_pm1, 100), (self.bank_pm1, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product2, 7), (self.product3, 1)], 'payments': [(self.cash_split_pm1, 70), (self.bank_pm1, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product1, 1), (self.product3, 5), (self.product2, 3)], 'payments': [(self.cash_split_pm1, 120), (self.bank_pm1, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0003'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 590, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 300, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 70, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 120, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }), - ((70, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 70, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 70, 'reconciled': True}, - ] - }), - ((120, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 120, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 120, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((300, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 300, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 300, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_rounding_method(self): - # set the cash rounding method - self.config.cash_rounding = True - self.config.rounding_method = self.env['account.cash.rounding'].create({ - 'name': 'add_invoice_line', - 'rounding': 0.05, - 'strategy': 'add_invoice_line', - 'profit_account_id': self.company['default_cash_difference_income_account_id'].copy().id, - 'loss_account_id': self.company['default_cash_difference_expense_account_id'].copy().id, - 'rounding_method': 'HALF-UP', - }) - - self.open_new_session() - - """ Test for orders: one with invoice - - 3 orders - - order 1, paid by cash - - order 2, paid by bank - - order 3, paid by bank, invoiced - - Orders - ====== - +---------+----------+---------------+----------+-----+-------+ - | order | payments | invoiced? | product | qty | total | - +---------+----------+---------------+----------+-----+-------+ - | order 1 | bank | no | product1 | 6 | 60 | - | | | | product4 | 4 | 39.84 | - +---------+----------+---------------+----------+-----+-------+ - | order 2 | bank | yes | product4 | 3 | 29.88 | - | | | | product2 | 20 | 400 | - +---------+----------+---------------+----------+-----+-------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | sale | -596,56 | - | pos receivable bank | 516,64 | - | Rounding applied | -0,01 | - +---------------------+---------+ - | Total balance | 0.0 | - +---------------------+---------+ - """ - - # create orders - orders = [] - - # create orders - orders = [] - orders.append(self.create_ui_order_data( - [(self.product4, 3), (self.product2, 20)], - payments=[(self.bank_pm1, 429.90)] - )) - - orders.append(self.create_ui_order_data( - [(self.product1, 6), (self.product4, 4)], - payments=[(self.bank_pm1, 99.85)] - )) - - # sync orders - self.env['pos.order'].sync_from_ui(orders) - - self.assertEqual(orders[0]['amount_return'], 0, msg='The amount return should be 0') - self.assertEqual(orders[1]['amount_return'], 0, msg='The amount return should be 0') - - # close the session - self.pos_session.action_pos_session_validate() - - # check values after the session is closed - session_account_move = self.pos_session.move_id - - rounding_line = session_account_move.line_ids.filtered(lambda line: line.name == 'Rounding line') - self.assertAlmostEqual(rounding_line.credit, 0.03, msg='The credit should be equals to 0.03') - - def test_correct_partner_on_invoice_receivables(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.cash_split_pm1 | self.bank_pm1 | self.bank_split_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments':[(self.cash_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments':[(self.bank_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments':[(self.cash_split_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0003'}, - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments':[(self.bank_split_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0004'}, - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments':[(self.cash_pm1, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0005'}, - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments':[(self.bank_pm1, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0006'}, - {'pos_order_lines_ui_args': [(self.product99, 1)], 'payments':[(self.cash_split_pm1, 99)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0007'}, - {'pos_order_lines_ui_args': [(self.product99, 1)], 'payments':[(self.bank_split_pm1, 99)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0008'}, - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments':[(self.bank_pm1, 100)], 'customer': self.other_customer, 'is_invoiced': True, 'uuid': '00100-010-0009'}, - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments':[(self.bank_pm1, 100)], 'customer': self.other_customer, 'is_invoiced': True, 'uuid': '00100-010-0010'}, - {'pos_order_lines_ui_args': [(self.product1, 10)], 'payments':[(self.bank_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0011'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_pm1, 100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '00100-010-0002': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '00100-010-0003': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_split_pm1, 100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '00100-010-0004': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_split_pm1, 100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '00100-010-0009': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 100), { - 'line_ids': [ - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '00100-010-0010': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 100), { - 'line_ids': [ - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '00100-010-0011': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 398, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 500, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 99, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 99, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 400, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }), - ((99, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 99, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 99, 'reconciled': True}, - ] - }), - ((200, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }), - ((99, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 99, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 99, 'reconciled': True}, - ] - }), - ((500, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 500, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 500, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_cash_register_if_no_order(self): - # Process one order with product3 - self.open_new_session(0) - session = self.pos_session - order_data = self.create_ui_order_data([(self.product3, 1)]) - amount_paid = order_data['amount_paid'] - with ( - self.assertLogs('odoo.addons.point_of_sale.models.pos_order') as cm, - unittest.mock.patch('odoo.addons.point_of_sale.models.pos_order.randrange', return_value=1996) - ): - self.env['ir.config_parameter'].sudo().set_bool('point_of_sale.log_order_data', True) - res = self.env['pos.order'].sync_from_ui([order_data]) - # Basic check for logs on order synchronization - order_log_str = self.env['pos.order']._get_order_log_representation(order_data) - odoo_order_id = res['pos.order'][0]['id'] - self.assertEqual(len(cm.output), 4) - self.assertEqual(cm.output[0], f"INFO:odoo.addons.point_of_sale.models.pos_order:PoS synchronisation #1996 started for PoS orders references: [{order_log_str}]") - self.assertTrue(cm.output[1].startswith(f'INFO:odoo.addons.point_of_sale.models.pos_order:PoS synchronisation #1996 processing order {order_log_str} order full data:')) - self.assertEqual(cm.output[2], f'INFO:odoo.addons.point_of_sale.models.pos_order:PoS synchronisation #1996 order {order_log_str} created pos.order #{odoo_order_id}') - self.assertEqual(cm.output[3], 'INFO:odoo.addons.point_of_sale.models.pos_order:PoS synchronisation #1996 finished') - - session.post_closing_cash_details(amount_paid) - session.close_session_from_ui() - - self.assertEqual(session.cash_register_balance_start, 0) - self.assertEqual(session.cash_register_balance_end_real, amount_paid) - - # Open/Close session without any order in cash control - self.open_new_session(amount_paid) - session = self.pos_session - session.post_closing_cash_details(amount_paid) - session.close_session_from_ui() - self.assertEqual(session.cash_register_balance_start, amount_paid) - self.assertEqual(session.cash_register_balance_end_real, amount_paid) - self.assertEqual(self.config.last_session_closing_cash, amount_paid) - - def test_start_balance_with_two_pos(self): - """ When having several POS with cash control, this tests ensures that each POS has its correct opening amount """ - - def open_and_check(pos_data): - self.config = pos_data['config'] - self.open_new_session(pos_data['amount_paid']) - session = self.pos_session - self.assertEqual(session.cash_register_balance_start, pos_data['amount_paid']) - - pos01_config = self.config - self.cash_journal = self.env['account.journal'].create( - {'name': 'CASH journal', 'type': 'cash', 'code': 'CSH00'}) - self.cash_payment_method = self.env['pos.payment.method'].create({ - 'name': 'Cash Test', - 'journal_id': self.cash_journal.id, - 'receivable_account_id': pos01_config.payment_method_ids.filtered(lambda s: s.is_cash_count)[ - 1].receivable_account_id.id - }) - pos02_config = pos01_config.copy({ - 'payment_method_ids': self.cash_payment_method - }) - pos01_data = {'config': pos01_config, 'p_qty': 1, 'amount_paid': 0} - pos02_data = {'config': pos02_config, 'p_qty': 3, 'amount_paid': 0} - - for pos_data in [pos01_data, pos02_data]: - open_and_check(pos_data) - session = self.pos_session - - order_data = self.create_ui_order_data([(self.product3, pos_data['p_qty'])]) - pos_data['amount_paid'] += order_data['amount_paid'] - self.env['pos.order'].sync_from_ui([order_data]) - - session.post_closing_cash_details(pos_data['amount_paid']) - session.close_session_from_ui() - - open_and_check(pos01_data) - open_and_check(pos02_data) - def test_pos_session_name_sequencing(self): """ This test check if the session name is correctly set according to the sequence """ @@ -897,194 +78,6 @@ def test_load_data_picks_the_company_website_domain(self): self.assertEqual(response['pos.config'][0]['_base_url'], company_website.domain) - def test_invoice_past_refund(self): - """ Test invoicing a past refund - - Orders - ====== - +------------------+----------+-----------+----------+-----+-------+ - | order | payments | invoiced? | product | qty | total | - +------------------+----------+-----------+----------+-----+-------+ - | order 1 | cash | no | product3 | 1 | 30 | - +------------------+----------+-----------+----------+-----+-------+ - | order 2 (return) | cash | no | product3 | -1 | -30 | - +------------------+----------+-----------+----------+-----+-------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | sale (sales) | -30 | - | sale (refund) | 30 | - +---------------------+---------+ - | Total balance | 0.0 | - +---------------------+---------+ - """ - def _before_closing_cb(): - # Return the order - order_to_return = self.pos_session.order_ids.filtered(lambda order: '12345-123-1234' in order.uuid) - order_to_return.refund() - refund_order = self.pos_session.order_ids.filtered(lambda order: order.state == 'draft') - - # Check if there's an amount to pay - self.assertAlmostEqual(refund_order.amount_total - refund_order.amount_paid, -30) - - # Pay the refund - context_make_payment = {"active_ids": [refund_order.id], "active_id": refund_order.id} - make_payment = self.env['pos.make.payment'].with_context(context_make_payment).create({ - 'payment_method_id': self.cash_pm1.id, - 'amount': -30, - }) - make_payment.check() - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product3, 1)], 'payments': [(self.cash_pm1, 30)], 'uuid': '12345-123-1234'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 30, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 30, 'credit': 0, 'reconciled': False}, - ], - }, - 'cash_statement': [], - 'bank_payments': [], - }, - }) - - closed_session = self.pos_session - self.assertTrue(closed_session.state == 'closed', 'Session should be closed.') - - return_to_invoice = closed_session.order_ids[1] - test_customer = self.env['res.partner'].create({'name': 'Test Customer'}) - new_session_date = return_to_invoice.date_order + relativedelta(days=2) - - with freeze_time(new_session_date): - # Create a new session after 2 days - self.open_new_session(0) - # Invoice the uninvoiced refund - return_to_invoice.write({'partner_id': test_customer.id}) - return_to_invoice.action_pos_order_invoice() - # Check the credit note - self.assertTrue(return_to_invoice.account_move, 'Invoice should be created.') - self.assertEqual(return_to_invoice.account_move.move_type, 'out_refund', 'Invoice should be a credit note.') - self.assertEqual(return_to_invoice.account_move.invoice_date, new_session_date.date(), 'Invoice date should be the same as the session it is created in.') - self.assertRecordValues(return_to_invoice.account_move, [{ - 'amount_untaxed': 30, - 'amount_tax': 0, - 'amount_total': 30, - }]) - self.assertRecordValues(return_to_invoice.account_move.line_ids, [ - {'account_id': self.sales_account.id, 'balance': 30}, - {'account_id': self.receivable_account.id, 'balance': -30}, - ]) - - def test_invoice_past_order(self): - # create 1 uninvoiced order then close the session - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product99, 1)], 'payments': [(self.bank_pm1, 99)], 'customer': False, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 99, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 99, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((99, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 99, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 99, 'reconciled': True}, - ] - }) - ], - }, - }) - - # keep reference of the closed session - closed_session = self.pos_session - self.assertTrue(closed_session.state == 'closed', 'Session should be closed.') - - order_to_invoice = closed_session.order_ids[0] - test_customer = self.env['res.partner'].create({'name': 'Test Customer'}) - - with freeze_time(fields.Datetime.now() + relativedelta(days=2)): - # create new session after 2 days - self.open_new_session(0) - # invoice the uninvoiced order - order_to_invoice.write({'partner_id': test_customer.id}) - order_to_invoice.action_pos_order_invoice() - # check invoice - invoice = order_to_invoice.account_move - self.assertTrue(invoice, 'Invoice should be created.') - self.assertNotEqual(invoice.invoice_date, order_to_invoice.date_order.date(), 'Invoice date should not be the same as order date since the session was closed.') - - # check that the payment date is set to the order date which - # is the real payment date and not to the invoice_date - payment = invoice.line_ids.full_reconcile_id.reconciled_line_ids.move_id - invoice - self.assertEqual(payment.date, order_to_invoice.date_order.date()) - - def test_invoice_past_order_affecting_taxes(self): - """ Test whether two taxes affecting each other don't trigger a recomputation on invoice generation - """ - # Create 1 uninvoiced order then close the session - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product_multi_tax, 1)], 'payments': [(self.bank_pm1, 117.72)], 'customer': False, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 8, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 9.72, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 117.72, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((117.72, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 117.72, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 117.72, 'reconciled': True}, - ] - }) - ], - }, - }) - - closed_session = self.pos_session - self.assertTrue(closed_session.state == 'closed', 'Session should be closed.') - - order_to_invoice = closed_session.order_ids[0] - test_customer = self.env['res.partner'].create({'name': 'Test Customer'}) - - # Create a new session - self.open_new_session(0) - # Invoice the uninvoiced order - order_to_invoice.write({'partner_id': test_customer.id}) - order_to_invoice.action_pos_order_invoice() - # Check the invoice for the lines - self.assertTrue(order_to_invoice.account_move, 'Invoice should be created.') - self.assertRecordValues(order_to_invoice.account_move.line_ids, [ - {'account_id': self.sales_account.id, 'balance': -100, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'balance': -8, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'balance': -9.72, 'reconciled': False}, - {'account_id': self.receivable_account.id, 'balance': 117.72, 'reconciled': True}, - ]) - def test_limited_products_loading(self): self.env['ir.config_parameter'].sudo().set_int('point_of_sale.limited_product_count', 3) @@ -1115,86 +108,6 @@ def get_top_product_ids(count): self.env['pos.order'].sync_from_ui([self.create_ui_order_data([(self.product3, 1)])]) self.assertEqual(get_top_product_ids(3), [self.product1.id, self.product2.id, self.product3.id]) - def test_closing_entry_by_product(self): - # set the Group by Product at Closing Entry - self.config.use_closing_entry_by_product = True - self.open_new_session() - - # 4 orders - - # Orders - # ====== - # +---------+----------+---------------+----------+-----+-------+ - # | order | payments | invoiced? | product | qty | total | - # +---------+----------+---------------+----------+-----+-------+ - # | order 1 | bank | no | product1 | 2 | 60 | - # | | | | product4 | 3 | 39.84 | - # +---------+----------+---------------+----------+-----+-------+ - # | order 2 | bank | yes | product4 | 1 | 29.88 | - # | | | | product2 | 5 | 400 | - # +---------+----------+---------------+----------+-----+-------+ - # | order 3 | bank | yes | product1 | 3 | 29.88 | - # | | | | product2 | 10 | 400 | - # +---------+----------+---------------+----------+-----+-------+ - # | order 4 | bank | yes | product1 | 5 | 29.88 | - # | | | | product0 | 10| 400 | - # +---------+----------+---------------+----------+-----+-------+ - - # Expected Output - # +---------------+-----------+ - # | invoice_line | Quantity | - # +---------------+-----------+ - # | Product 0 | 10 | - # +---------------+-----------+ - # | Product 1 | 10 | - # +---------------+-----------+ - # | Product 2 | 15 | - # +---------------+-----------+ - # | Product 4 | 4 | - # +---------------+-----------+ - - # create orders - orders = [] - - # create orders - orders = [] - orders.append(self.create_ui_order_data( - [(self.product1, 2), (self.product4, 3)], - payments=[(self.bank_pm1, 49.88)] - )) - orders.append(self.create_ui_order_data( - [(self.product4, 1), (self.product2, 5)], - payments=[(self.bank_pm1, 109.96)] - )) - orders.append(self.create_ui_order_data( - [(self.product1, 3), (self.product2, 10)], - payments=[(self.bank_pm1, 230)] - )) - orders.append(self.create_ui_order_data( - [(self.product1, 5), (self.product0, 10)], - payments=[(self.bank_pm1, 50)] - )) - - # sync orders - self.env['pos.order'].sync_from_ui(orders) - # close the session - self.pos_session.action_pos_session_validate() - - # check values after the session is closed - session_account_move = self.pos_session.move_id - - # Define expected quantities for each product - expected_product_quantity = { - self.product0: 10, - self.product1: 10, - self.product2: 15, - self.product4: 4, - } - # Iterate through invoice lines and assert the expected quantities - for i in session_account_move.line_ids: - if i.product_id and expected_product_quantity.get(i.product_id): - self.assertEqual(i.quantity, expected_product_quantity.get(i.product_id), f"Unexpected quantity for {i.product_id.name}") - def test_pos_payment_method_copy(self): """ Test POS payment method copy: @@ -1228,21 +141,26 @@ def test_single_config_global_invoice(self): # sync orders self.env['pos.order'].sync_from_ui(orders) # close the session - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() pos_orders = self.env['pos.order'].search([]) # set customer for the orders pos_orders.write({'partner_id': self.customer.id}) # create consolidated invoice - self.env['pos.make.invoice'].create({"consolidated_billing": True}).with_context({"active_ids": pos_orders.ids}).action_create_invoices() + self.env['pos.make.invoice'].create({ + "consolidated_billing": True, + }).with_context({ + "active_ids": pos_orders.ids, + }).action_create_invoices() # check if have single invoice self.assertEqual(len(pos_orders), 2) self.assertEqual(len(pos_orders.account_move), 1) self.assertEqual(pos_orders.account_move.partner_id, self.customer) self.assertEqual(pos_orders.account_move.amount_total, sum(pos_orders.mapped('amount_total'))) - self.assertEqual(pos_orders.account_move.payment_state, 'paid') + self.assertEqual(pos_orders.account_move.payment_state, 'in_payment') self.assertEqual(pos_orders.account_move.state, 'posted') + self.assertEqual(pos_orders.account_move.amount_residual, 0) def test_multi_config_global_invoice(self): self.open_new_session() @@ -1256,7 +174,7 @@ def test_multi_config_global_invoice(self): payments=[(self.bank_pm1, 50)] )) self.env['pos.order'].sync_from_ui(orders) - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() # open new session & create orders self.open_new_session() @@ -1270,40 +188,28 @@ def test_multi_config_global_invoice(self): payments=[(self.bank_pm1, 109.96)] )) self.env['pos.order'].sync_from_ui(orders2) - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() pos_orders = self.env['pos.order'].search([]) # set customer for the orders pos_orders.write({'partner_id': self.customer.id}) # create consolidated invoice - self.env['pos.make.invoice'].create({"consolidated_billing": True}).with_context({"active_ids": pos_orders.ids}).action_create_invoices() + self.env['pos.make.invoice'].create({ + "consolidated_billing": True, + }).with_context({ + "active_ids": pos_orders.ids, + }).action_create_invoices() # check if have single invoice self.assertEqual(len(pos_orders), 4) + self.assertTrue(all(order.state == 'done' for order in pos_orders)) self.assertEqual(len(pos_orders.account_move), 1) + self.assertNotEqual(self.pos_session.move_ids, pos_orders.account_move) self.assertEqual(pos_orders.account_move.partner_id, self.customer) self.assertEqual(pos_orders.account_move.amount_total, round(sum(pos_orders.mapped('amount_total')), 2)) - self.assertEqual(pos_orders.account_move.payment_state, 'paid') + self.assertEqual(pos_orders.account_move.payment_state, 'in_payment') self.assertEqual(pos_orders.account_move.state, 'posted') - - def test_double_syncing_same_order(self): - """ Test that double syncing the same order doesn't create duplicates records - """ - self.open_new_session() - - # Create an order - order_data = self.create_ui_order_data([(self.product1, 1)], payments=[(self.cash_pm1, 10)], customer=self.customer, is_invoiced=True) - order_data['access_token'] = '0123456789' - res = self.env['pos.order'].sync_from_ui([order_data]) - order_id = res['pos.order'][0]['id'] - - # Sync the same order again - res = self.env['pos.order'].sync_from_ui([order_data]) - self.assertEqual(res['pos.order'][0]['id'], order_id, 'Syncing the same order should not create a new one') - - order = self.env['pos.order'].browse(order_id) - self.assertEqual(len(order.payment_ids), 1, 'Order should have one payment') - self.assertEqual(self.env['account.move'].search_count([('pos_order_ids', 'in', order.ids)]), 1, 'Order should have one invoice') + self.assertEqual(pos_orders.account_move.amount_residual, 0) def test_pos_archived_combination(self): product = self.env['product.template'].create({ @@ -1430,6 +336,7 @@ def test_cannot_archive_journal_linked_to_pos_payment_method(self): }) test_payment_method = self.env['pos.payment.method'].create({ 'name': 'Test PM', + 'type': 'cash', 'journal_id': test_journal.id, 'receivable_account_id': self.cash_pm1.receivable_account_id.id, }) diff --git a/addons/point_of_sale/tests/test_pos_controller.py b/addons/point_of_sale/tests/test_pos_controller.py index 621866cfbf0752..1701b21896c4f9 100644 --- a/addons/point_of_sale/tests/test_pos_controller.py +++ b/addons/point_of_sale/tests/test_pos_controller.py @@ -61,7 +61,7 @@ def test_qr_code_receipt(self): } self.url_open(f'/pos/ticket/validate?access_token={self.pos_order.access_token}', data=get_invoice_data) self.assertEqual(self.env['res.partner'].sudo().search_count([('name', '=', 'AAA Partner')]), 1) - self.assertTrue(self.pos_order.is_invoiced, "The pos order should have an invoice") + self.assertTrue(self.pos_order.is_singly_invoiced, "The pos order should have an invoice") self.assertTrue(len(self.pos_order.pos_reference) >= 12, "The pos reference should not be less than 12 characters") def test_qr_code_receipt_user_connected(self): @@ -107,7 +107,7 @@ def test_qr_code_receipt_user_connected(self): }) self.main_pos_config.current_session_id.close_session_from_ui() res = self.url_open(f'/pos/ticket/validate?access_token={self.pos_order.access_token}', timeout=30000) - self.assertTrue(self.pos_order.is_invoiced, "The pos order should have an invoice") + self.assertTrue(self.pos_order.is_singly_invoiced, "The pos order should have an invoice") self.assertTrue("my/invoices" in res.url) def test_qr_code_receipt_user_not_connected(self): @@ -150,6 +150,7 @@ def test_qr_code_receipt_user_not_connected(self): self.main_pos_config.current_session_id.close_session_from_ui() self.start_tour('/pos/ticket', 'invoicePoSOrderWithSelfInvocing', login=None) self.assertTrue(self.pos_order.account_move, "The pos order should have an invoice after self invoicing") + self.assertNotEqual(self.pos_order.account_move, self.pos_order.session_id.move_ids) def test_qr_code_receipt_user_updated(self): """This test make sure that when the user is already connected he correctly gets redirected to the invoice.""" diff --git a/addons/point_of_sale/tests/test_pos_invoice_consolidation.py b/addons/point_of_sale/tests/test_pos_invoice_consolidation.py index e1c19ad0ae95a5..ec02437658f1fe 100644 --- a/addons/point_of_sale/tests/test_pos_invoice_consolidation.py +++ b/addons/point_of_sale/tests/test_pos_invoice_consolidation.py @@ -127,18 +127,24 @@ def test_consolidation_non_cash_with_cash_rounding_enabled(self): 'rounding_method': rounding.id, }) - non_cash_pm = self.config.payment_method_ids.filtered(lambda pm: not pm.is_cash_count)[:1] + non_cash_pm = self.config.payment_method_ids.filtered(lambda pm: pm.type != 'cash')[:1] self.assertTrue(non_cash_pm, "Need at least one non-cash payment method on the POS config.") - + self.product2.lst_price = 9.99 with self.with_user(self.user1.login): orders = self._create_orders([ {'pos_order_lines_ui_args': [(self.product1, 1)], 'customer': self.customer, 'is_invoiced': False}, - {'pos_order_lines_ui_args': [(self.product1, -1)], 'customer': self.customer, 'is_invoiced': False}, + { + 'pos_order_lines_ui_args': [(self.product2, 1)], + 'customer': self.customer, + 'is_invoiced': False, + 'payments': [(self.cash_pm1, 10)]}, ]) orders = sum(orders.values(), self.env['pos.order']) - orders.payment_ids.write({'payment_method_id': non_cash_pm.id}) - orders.payment_ids[:1].write({'amount': orders.payment_ids[:1].amount + 1}) - + self.assertEqual(len(orders), 2, "Should have created 2 orders") + self.assertEqual(orders.mapped('amount_total'), [10.0, 9.99]) + self.assertEqual(orders.mapped('amount_paid'), [10.0, 10.0]) + self.assertEqual(orders.mapped('amount_difference'), [0.0, 0.01]) + self.assertEqual(orders.mapped('amount_return'), [0.0, 0.0]) self.env['pos.make.invoice'].create({'consolidated_billing': True}).with_context(active_ids=orders.ids).action_create_invoices() self.assertEqual(len(orders.account_move), 1) diff --git a/addons/point_of_sale/tests/test_pos_margin.py b/addons/point_of_sale/tests/test_pos_margin.py index cdbb0db51cb6c1..2f6e23c7d7874e 100644 --- a/addons/point_of_sale/tests/test_pos_margin.py +++ b/addons/point_of_sale/tests/test_pos_margin.py @@ -45,7 +45,7 @@ def test_positive_margin(self): self.assertEqual(round(self.pos_session.order_ids[2].margin_percent, 2), 0.42) # close session - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() def test_negative_margin(self): """ @@ -77,7 +77,7 @@ def test_negative_margin(self): self.assertEqual(round(self.pos_session.order_ids[2].margin_percent, 2), -0.92) # close session - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() def test_full_margin(self): """ @@ -109,7 +109,7 @@ def test_full_margin(self): self.assertEqual(self.pos_session.order_ids[2].margin_percent, 1) # close session - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() def test_tax_margin(self): """ @@ -143,7 +143,7 @@ def test_tax_margin(self): self.assertEqual(round(self.pos_session.order_ids[2].margin_percent, 2), 0.42) # close session - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() def test_other_currency_margin(self): """ @@ -181,7 +181,7 @@ def test_other_currency_margin(self): self.assertEqual(round(self.pos_session.order_ids[2].margin_percent, 2), 0.42) # close session - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() # set the config back self.config = current_config @@ -223,7 +223,7 @@ def test_tax_and_other_currency_margin(self): self.assertEqual(self.pos_session.order_ids[2].margin_percent, 0.4167) # close session - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() # set the config back self.config = current_config @@ -240,9 +240,11 @@ def test_return_margin(self): self.open_new_session() # create orders - orders = [self.create_ui_order_data([(product1, -1)]), - self.create_ui_order_data([(product2, -1)]), - self.create_ui_order_data([(product1, -2), (product2, -2)])] + orders = [ + self.create_ui_order_data([(product1, -1)], pos_order_ui_args={'is_refund': True}), + self.create_ui_order_data([(product2, -1)], pos_order_ui_args={'is_refund': True}), + self.create_ui_order_data([(product1, -2), (product2, -2)], pos_order_ui_args={'is_refund': True}), + ] # sync orders self.env['pos.order'].sync_from_ui(orders) @@ -258,4 +260,4 @@ def test_return_margin(self): self.assertEqual(round(self.pos_session.order_ids[2].margin_percent, 2), 0.42) # close session - self.pos_session.action_pos_session_validate() + self.pos_session.close_session_from_ui() diff --git a/addons/point_of_sale/tests/test_pos_multiple_receivable_accounts.py b/addons/point_of_sale/tests/test_pos_multiple_receivable_accounts.py deleted file mode 100644 index 4093197e33556d..00000000000000 --- a/addons/point_of_sale/tests/test_pos_multiple_receivable_accounts.py +++ /dev/null @@ -1,287 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import odoo - -from odoo import tools -from odoo.addons.point_of_sale.tests.common import TestPoSCommon - -@odoo.tests.tagged('post_install', '-at_install') -class TestPoSMultipleReceivableAccounts(TestPoSCommon): - """ Test for invoiced orders with customers having receivable account different from default - - Thus, for this test, there are two receivable accounts involved and are set in the - customers. - self.customer -> self.receivable_account - self.other_customer -> self.other_receivable_account - - ADDITIONALLY, this tests different sales account on the products. - - NOTE That both receivable accounts above are different from the pos receivable account. - """ - - def setUp(self): - super(TestPoSMultipleReceivableAccounts, self).setUp() - self.config = self.basic_config - self.product1 = self.create_product( - 'Product 1', - self.categ_basic, - lst_price=10.99, - standard_price=5.0, - tax_ids=self.taxes['tax7'].ids, - ) - self.product2 = self.create_product( - 'Product 2', - self.categ_basic, - lst_price=19.99, - standard_price=10.0, - tax_ids=self.taxes['tax10'].ids, - sale_account=self.other_sale_account, - ) - self.product3 = self.create_product( - 'Product 3', - self.categ_basic, - lst_price=30.99, - standard_price=15.0, - tax_ids=self.taxes['tax_group_7_10'].ids, - ) - - def test_01_invoiced_order_from_other_customer(self): - """ - Orders - ====== - +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ - | order 1 | cash | no | product1 | 10 | 109.9 | 7.69 [7%] | 117.59 | - | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.9 | - | | | | product3 | 10 | 281.73 | 19.72 [7%] + 28.17 [10%] | 329.62 | - +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ - | order 2 | bank | no | product1 | 5 | 54.95 | 3.85 [7%] | 58.80 | - | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ - | order 3 | bank | yes | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - | | | | product3 | 5 | 140.86 | 9.86 [7%] + 14.09 [10%] | 164.81 | - +---------+----------+-----------+----------+-----+---------+--------------------------+--------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | sale_account | -164.85 | - | sale_account | -281.73 | - | other_sale_account | -272.59 | - | tax 7% | -31.26 | - | tax 10% | -55.43 | - | pos receivable cash | 647.11 | - | pos receivable bank | 423.51 | - | other receivable | -264.76 | - +---------------------+---------+ - | Total balance | 0.00 | - +---------------------+---------+ - """ - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - # check if there is one invoiced order - self.assertEqual(len(self.pos_session.order_ids.filtered(lambda order: order.account_move)), 1, 'There should only be one invoiced order.') - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 10), (self.product3, 10)], 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product1, 5), (self.product2, 5)], 'payments': [(self.bank_pm1, 158.75)], 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product2, 5), (self.product3, 5)], 'payments': [(self.bank_pm1, 264.76)], 'is_invoiced': True, 'customer': self.other_customer, 'uuid': '09876-098-0987'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': { - '09876-098-0987': { - 'invoice': { - 'line_ids_predicate': lambda line: line.account_id in self.other_sale_account | self.sales_account | self.other_receivable_account, - 'line_ids': [ - {'account_id': self.other_sale_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 90.86, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 140.87, 'reconciled': False}, - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 264.76, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 264.76), { - 'line_ids': [ - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 264.76, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 264.76, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 31.26, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 55.44, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 164.85, 'reconciled': False}, - {'account_id': self.other_sale_account.id, 'partner_id': False, 'debit': 0, 'credit': 272.59, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 281.72, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 423.51, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 647.11, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 264.76, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((647.11, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 647.11, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 647.11, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((423.51, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 423.51, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 423.51, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_02_all_orders_invoiced_mixed_customers(self): - """ - Orders - ====== - +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ - | order 1 | cash | yes, other_customer | product1 | 10 | 109.90 | 7.69 [7%] | 117.59 | - | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.90 | - | | | | product3 | 10 | 281.73 | 19.72 [7%] + 28.17 [10%] | 329.62 | - +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ - | order 2 | bank | yes, customer | product1 | 5 | 54.95 | 3.85 [7%] | 58.80 | - | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ - | order 3 | bank | yes, other customer | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - | | | | product3 | 5 | 140.86 | 9.86 [7%] + 14.09 [10%] | 164.81 | - +---------+----------+---------------------+----------+-----+---------+--------------------------+--------+ - - Expected Result - =============== - +----------------------+---------+ - | account | balance | - +----------------------+---------+ - | pos receivable cash | 647.11 | - | pos receivable bank | 423.51 | - | received bank | -423.51 | - | received cash | -647.11 | - +----------------------+---------+ - | Total balance | 0.00 | - +----------------------+---------+ - - """ - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - # check if there is one invoiced order - self.assertEqual(len(self.pos_session.order_ids.filtered(lambda order: order.account_move)), 3, 'All orders should be invoiced.') - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 10), (self.product3, 10)], 'is_invoiced': True, 'customer': self.other_customer, 'uuid': '09876-098-0987'}, - {'pos_order_lines_ui_args': [(self.product1, 5), (self.product2, 5)], 'payments': [(self.bank_pm1, 158.75)], 'is_invoiced': True, 'customer': self.customer, 'uuid': '09876-098-0988'}, - {'pos_order_lines_ui_args': [(self.product2, 5), (self.product3, 5)], 'payments': [(self.bank_pm1, 264.76)], 'is_invoiced': True, 'customer': self.other_customer, 'uuid': '09876-098-0989'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': { - '09876-098-0987': { - 'invoice': { - 'line_ids_predicate': lambda line: line.account_id in self.other_sale_account | self.sales_account | self.other_receivable_account, - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 109.90, 'reconciled': False}, - {'account_id': self.other_sale_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 181.73, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 281.72, 'reconciled': False}, - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 647.11, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_pm1, 647.11), { - 'line_ids': [ - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 647.11, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 647.11, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '09876-098-0988': { - 'invoice': { - 'line_ids_predicate': lambda line: line.account_id in self.other_sale_account | self.sales_account | self.c1_receivable, - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 54.95, 'reconciled': False}, - {'account_id': self.other_sale_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 90.86, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 158.75, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 158.75), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 158.75, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 158.75, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '09876-098-0989': { - 'invoice': { - 'line_ids_predicate': lambda line: line.account_id in self.other_sale_account | self.sales_account | self.other_receivable_account, - 'line_ids': [ - {'account_id': self.other_sale_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 90.86, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 140.87, 'reconciled': False}, - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 264.76, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 264.76), { - 'line_ids': [ - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 264.76, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 264.76, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 423.51, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 647.11, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 647.11, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 423.51, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((647.11, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 647.11, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 647.11, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((423.51, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 423.51, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 423.51, 'reconciled': True}, - ] - }), - ], - }, - }) diff --git a/addons/point_of_sale/tests/test_pos_other_currency_config.py b/addons/point_of_sale/tests/test_pos_other_currency_config.py index db50a9e7903933..e80307a91286a3 100644 --- a/addons/point_of_sale/tests/test_pos_other_currency_config.py +++ b/addons/point_of_sale/tests/test_pos_other_currency_config.py @@ -34,206 +34,6 @@ def test_01_check_product_cost(self): self.assertAlmostEqual(self.config.pricelist_id._get_product_price(self.product3, 1), 15.00) self.assertAlmostEqual(self.config.pricelist_id._get_product_price(self.product7, 1), 3.50) - def test_02_orders_without_invoice(self): - """ orders without invoice - - Orders - ====== - +---------+----------+-----------+----------+-----+-------+ - | order | payments | invoiced? | product | qty | total | - +---------+----------+-----------+----------+-----+-------+ - | order 1 | cash | no | product1 | 10 | 50 | - | | | | product2 | 10 | 129.9 | - | | | | product3 | 10 | 150 | - +---------+----------+-----------+----------+-----+-------+ - | order 2 | cash | no | product1 | 5 | 25 | - | | | | product2 | 5 | 64.95 | - +---------+----------+-----------+----------+-----+-------+ - | order 3 | bank | no | product2 | 5 | 64.95 | - | | | | product3 | 5 | 75 | - +---------+----------+-----------+----------+-----+-------+ - - Expected Result - =============== - +---------------------+---------+-----------------+ - | account | balance | amount_currency | - +---------------------+---------+-----------------+ - | sale_account | -1119.6 | -559.80 | - | pos receivable bank | 279.9 | 139.95 | - | pos receivable cash | 839.7 | 419.85 | - +---------------------+---------+-----------------+ - | Total balance | 0.0 | 0.00 | - +---------------------+---------+-----------------+ - """ - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - self._run_test({ - 'payment_methods': self.cash_pm2 | self.bank_pm2, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 10), (self.product3, 10)], 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product1, 5), (self.product2, 5)], 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product2, 5), (self.product3, 5)], 'payments': [(self.bank_pm2, 139.95)], 'uuid': '00100-010-0003'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 1119.6, 'reconciled': False, 'amount_currency': -559.80}, - {'account_id': self.bank_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 279.9, 'credit': 0, 'reconciled': True, 'amount_currency': 139.95}, - {'account_id': self.cash_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 839.7, 'credit': 0, 'reconciled': True, 'amount_currency': 419.85}, - ], - }, - 'cash_statement': [ - ((419.85, ), { - 'line_ids': [ - {'account_id': self.cash_pm2.journal_id.default_account_id.id, 'partner_id': False, 'debit': 839.7, 'credit': 0, 'reconciled': False, 'amount_currency': 419.85}, - {'account_id': self.cash_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 839.7, 'reconciled': True, 'amount_currency': -419.85}, - ] - }), - ], - 'bank_payments': [ - ((139.95, ), { - 'line_ids': [ - {'account_id': self.bank_pm2.outstanding_account_id.id, 'partner_id': False, 'debit': 279.9, 'credit': 0, 'reconciled': False, 'amount_currency': 139.95}, - {'account_id': self.bank_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 279.9, 'reconciled': True, 'amount_currency': -139.95}, - ] - }), - ], - }, - }) - - def test_03_orders_with_invoice(self): - """ orders with invoice - - Orders - ====== - +---------+----------+---------------+----------+-----+-------+ - | order | payments | invoiced? | product | qty | total | - +---------+----------+---------------+----------+-----+-------+ - | order 1 | cash | no | product1 | 10 | 50 | - | | | | product2 | 10 | 129.9 | - | | | | product3 | 10 | 150 | - +---------+----------+---------------+----------+-----+-------+ - | order 2 | cash | yes, customer | product1 | 5 | 25 | - | | | | product2 | 5 | 64.95 | - +---------+----------+---------------+----------+-----+-------+ - | order 3 | bank | yes, customer | product2 | 5 | 64.95 | - | | | | product3 | 5 | 75 | - +---------+----------+---------------+----------+-----+-------+ - - Expected Result - =============== - +---------------------+---------+-----------------+ - | account | balance | amount_currency | - +---------------------+---------+-----------------+ - | sale_account | -659.8 | -329.90 | - | pos receivable bank | 279.9 | 139.95 | - | pos receivable cash | 839.7 | 419.85 | - | invoice receivable | -179.9 | -89.95 | - | invoice receivable | -279.9 | -139.95 | - +---------------------+---------+-----------------+ - | Total balance | 0.0 | 0.00 | - +---------------------+---------+-----------------+ - """ - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - self._run_test({ - 'payment_methods': self.cash_pm2 | self.bank_pm2, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 10), (self.product3, 10)], 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product1, 5), (self.product2, 5)], 'is_invoiced': True, 'customer': self.customer, 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product2, 5), (self.product3, 5)], 'payments': [(self.bank_pm2, 139.95)], 'is_invoiced': True, 'customer': self.customer, 'uuid': '00100-010-0003'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': { - '00100-010-0002': { - 'payments': [ - ((self.cash_pm2, 89.95), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 179.90, 'reconciled': True, 'amount_currency': -89.95}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 179.90, 'credit': 0, 'reconciled': False, 'amount_currency': 89.95}, - ] - }), - ], - }, - '00100-010-0003': { - 'payments': [ - ((self.bank_pm2, 139.95), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 279.9, 'reconciled': True, 'amount_currency': -139.95}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 279.9, 'credit': 0, 'reconciled': False, 'amount_currency': 139.95}, - ] - }), - ], - }, - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 659.8, 'reconciled': False, 'amount_currency': -329.90}, - {'account_id': self.bank_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 279.9, 'credit': 0, 'reconciled': True, 'amount_currency': 139.95}, - {'account_id': self.cash_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 839.7, 'credit': 0, 'reconciled': True, 'amount_currency': 419.85}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 179.90, 'reconciled': True, 'amount_currency': -89.95}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 279.9, 'reconciled': True, 'amount_currency': -139.95}, - ], - }, - 'cash_statement': [ - ((419.85, ), { - 'line_ids': [ - {'account_id': self.cash_pm2.journal_id.default_account_id.id, 'partner_id': False, 'debit': 839.7, 'credit': 0, 'reconciled': False, 'amount_currency': 419.85}, - {'account_id': self.cash_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 839.7, 'reconciled': True, 'amount_currency': -419.85}, - ] - }), - ], - 'bank_payments': [ - ((139.95, ), { - 'line_ids': [ - {'account_id': self.bank_pm2.outstanding_account_id.id, 'partner_id': False, 'debit': 279.9, 'credit': 0, 'reconciled': False, 'amount_currency': 139.95}, - {'account_id': self.bank_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 279.9, 'reconciled': True, 'amount_currency': -139.95}, - ] - }), - ], - }, - }) - - def test_05_tax_base_amount(self): - self._run_test({ - 'payment_methods': self.cash_pm2, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product7, 7)], 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 3.43, 'reconciled': False, 'amount_currency': -1.715, 'tax_base_amount': 49}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 49, 'reconciled': False, 'amount_currency': -24.5, 'tax_base_amount': 0}, - {'account_id': self.cash_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 52.43, 'credit': 0, 'reconciled': True, 'amount_currency': 26.215, 'tax_base_amount': 0}, - ], - }, - 'cash_statement': [ - ((26.215, ), { - 'line_ids': [ - {'account_id': self.cash_pm2.journal_id.default_account_id.id, 'partner_id': False, 'debit': 52.43, 'credit': 0, 'reconciled': False, 'amount_currency': 26.215}, - {'account_id': self.cash_pm2.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 52.43, 'reconciled': True, 'amount_currency': -26.215}, - ] - }), - ], - 'bank_payments': [], - }, - }) - def test_bank_journal_balance(self): """Verify that debit and credit are balanced when adding a difference to the bank.""" @@ -271,7 +71,7 @@ def test_bank_journal_balance(self): order_payment.with_context(**payment_context).check() # Close session with counted +10 for bank compared with expected - session_id.action_pos_session_closing_control(bank_payment_method_diffs={self.bank_pm2.id: 10.00}) # Real 20, expected 10, diff 10 + session_id.close_session_from_ui() # Real 20, expected 10, diff 10 # Check debit/credit session's balance for move in session_id._get_related_account_moves(): diff --git a/addons/point_of_sale/tests/test_pos_products_with_tax.py b/addons/point_of_sale/tests/test_pos_products_with_tax.py index 9f3d278e6ecd7c..7a13e2d57b22d8 100644 --- a/addons/point_of_sale/tests/test_pos_products_with_tax.py +++ b/addons/point_of_sale/tests/test_pos_products_with_tax.py @@ -45,514 +45,6 @@ def setUp(self): tax_ids=[self.taxes['tax_fixed006'].id, self.taxes['tax_fixed012'].id, self.taxes['tax21'].id], ) - def test_orders_no_invoiced(self): - """ Test for orders without invoice - - Orders - ====== - +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ - | order 1 | cash | no | product1 | 10 | 100 | 7 | 107 | - | | | | product2 | 5 | 90.91 | 9.09 | 100 | - +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ - | order 2 | cash | no | product2 | 7 | 127.27 | 12.73 | 140 | - | | | | product3 | 4 | 109.09 | 10.91[10%] + 7.64[7%] | 127.64 | - +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ - | order 3 | bank | no | product1 | 1 | 10 | 0.7 | 10.7 | - | | | | product2 | 3 | 54.55 | 5.45 | 60 | - | | | | product3 | 5 | 136.36 | 13.64[10%] + 9.55[7%] | 159.55 | - +---------+----------+-----------+----------+-----+---------+-----------------------+--------+ - - Calculated taxes - ================ - total tax 7% only + group tax (10+7%) - (7 + 0.7) + (7.64 + 9.55) = 7.7 + 17.19 = 24.89 - total tax 10% only + group tax (10+7%) - (9.09 + 12.73 + 5.45) + (10.91 + 13.64) = 27.27 + 24.55 = 51.82 - - Thus, manually_calculated_taxes = (-24,89, -51.82) - """ - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 5)], 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product2, 7), (self.product3, 4)], 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product1, 1), (self.product3, 5), (self.product2, 3)], 'payments': [(self.bank_pm1, 230.25)], 'uuid': '00100-010-0003'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 24.89, 'reconciled': False, 'display_type': 'tax'}, - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 51.82, 'reconciled': False, 'display_type': 'tax'}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 110, 'reconciled': False, 'display_type': 'product'}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 272.73, 'reconciled': False, 'display_type': 'product'}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 245.45, 'reconciled': False, 'display_type': 'product'}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 230.25, 'credit': 0, 'reconciled': True, 'display_type': 'payment_term'}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 474.64, 'credit': 0, 'reconciled': True, 'display_type': 'payment_term'}, - ], - }, - 'cash_statement': [ - ((474.64, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 474.64, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 474.64, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((230.25, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 230.25, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 230.25, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_orders_with_invoiced(self): - """ Test for orders: one with invoice - - Orders - ====== - +---------+----------+---------------+----------+-----+---------+---------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+---------------+----------+-----+---------+---------------+--------+ - | order 1 | cash | no | product1 | 6 | 60 | 4.2 | 64.2 | - | | | | product2 | 3 | 54.55 | 5.45 | 60 | - | | | | product3 | 1 | 27.27 | 2.73 + 1.91 | 31.91 | - +---------+----------+---------------+----------+-----+---------+---------------+--------+ - | order 2 | bank | no | product1 | 1 | 10 | 0.7 | 10.7 | - | | | | product2 | 20 | 363.64 | 36.36 | 400 | - +---------+----------+---------------+----------+-----+---------+---------------+--------+ - | order 3 | bank | yes, customer | product1 | 10 | 100 | 7 | 107 | - | | | | product3 | 10 | 272.73 | 27.27 + 19.09 | 319.09 | - +---------+----------+---------------+----------+-----+---------+---------------+--------+ - - Calculated taxes - ================ - total tax 7% only - 4.2 + 0.7 => 4.9 + 1.91 = 6.81 - total tax 10% only - 5.45 + 36.36 => 41.81 + 2.73 = 44.54 - - Thus, manually_calculated_taxes = (-6.81, -44.54) - """ - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(4, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - # check account move in the invoiced order - invoiced_orders = self.pos_session.order_ids.filtered(lambda order: order.is_invoiced) - self.assertEqual(2, len(invoiced_orders), 'Only one order is invoiced in this test.') - invoices = invoiced_orders.mapped('account_move') - self.assertAlmostEqual(sum(invoices.mapped('amount_total')), 481.08) - - def _after_closing_cb(): - session_move = self.pos_session.move_id - tax_lines = session_move.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) - - manually_calculated_taxes = (-6.81, -44.54) - self.assertAlmostEqual(sum(manually_calculated_taxes), sum(tax_lines.mapped('balance'))) - for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): - self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined.') - - base_amounts = (97.27, 445.46) # computation does not include invoiced order. - self.assertAlmostEqual(sum(base_amounts), sum(tax_lines.mapped('tax_base_amount'))) - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product3, 1), (self.product1, 6), (self.product2, 3)], 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product2, 20), (self.product1, 1)], 'payments': [(self.bank_pm1, 410.7)], 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product3, 10)], 'payments': [(self.bank_pm1, 426.09)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '09876-098-0987'}, - {'pos_order_lines_ui_args': [(self.product4, 1)], 'payments': [(self.bank_pm1, 54.99)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0004'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': { - '09876-098-0987': { - 'payments': [ - ((self.bank_pm1, 426.09), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 426.09, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 426.09, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '00100-010-0004': { - 'payments': [ - ((self.bank_pm1, 54.99), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 54.99, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 54.99, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - }, - 'after_closing_cb': _after_closing_cb, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 6.81, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 44.54, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 27.27, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 70, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 418.19, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 891.78, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 156.11, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 481.08, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((156.11, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 156.11, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 156.11, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((891.78, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 891.78, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 891.78, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_return_order(self): - """ Test return order - - Order (invoiced) - ====== - +----------+----------+---------------+----------+-----+---------+-------------+-------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +----------+----------+---------------+----------+-----+---------+-------------+-------+ - | order 1 | cash | yes, customer | product1 | 3 | 30 | 2.1 | 32.1 | - | | | | product2 | 2 | 36.36 | 3.64 | 40 | - | | | | product3 | 1 | 27.27 | 2.73 + 1.91 | 31.91 | - +----------+----------+---------------+----------+-----+---------+-------------+-------+ - - The order is invoiced so the tax of the invoiced order is in the account_move of the order. - However, the return order is not invoiced, thus, the journal items are in the session_move, - which will contain the tax lines of the returned products. - - manually_calculated_taxes = (4.01, 6.37) - """ - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(1, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - # return order - order_to_return = self.pos_session.order_ids.filtered(lambda order: '12345-123-1234' in order.uuid) - order_to_return.refund() - - refund_order = self.pos_session.order_ids.filtered(lambda order: order.state == 'draft') - context_make_payment = {"active_ids": [refund_order.id], "active_id": refund_order.id} - make_payment = self.env['pos.make.payment'].with_context(context_make_payment).create({ - 'payment_method_id': self.cash_pm1.id, - 'amount': -104.01, - }) - make_payment.check() - self.assertEqual(refund_order.state, 'paid', 'Payment is registered, order should be paid.') - self.assertAlmostEqual(refund_order.amount_paid, -104.01, msg='Amount paid for return order should be negative.') - - def _after_closing_cb(): - manually_calculated_taxes = (4.01, 6.36) # should be positive since it is return order - tax_lines = self.pos_session.move_id.line_ids.filtered(lambda line: line.account_id == self.tax_received_account) - self.assertAlmostEqual(sum(manually_calculated_taxes), sum(tax_lines.mapped('balance'))) - for t1, t2 in zip(sorted(manually_calculated_taxes), sorted(tax_lines.mapped('balance'))): - self.assertAlmostEqual(t1, t2, msg='Taxes should be correctly combined and should be debit.') - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 3), (self.product2, 2), (self.product3, 1)], 'payments': [(self.cash_pm1, 104.01)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '12345-123-1234'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': { - '12345-123-1234': { - 'payments': [ - ((self.cash_pm1, 104.01), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 104.01, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 104.01, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - }, - 'after_closing_cb': _after_closing_cb, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 4.01, 'credit': 0, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 6.36, 'credit': 0, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 30, 'credit': 0, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 36.37, 'credit': 0, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 27.27, 'credit': 0, 'reconciled': False}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 104.01, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [], - }, - }) - - def test_pos_create_correct_account_move(self): - """ Test for orders with global rounding disabled - - Orders - ====== - +---------+----------+-----------+----------+------+----------+------------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+-----------+----------+------+----------+------------------+--------+ - | order 1 | cash | no | product1 | 1 | 10.0 | 2.10 | 12.10 | - | | | | product2 | -1 | -5.0 | -1.05 | -6.05 | - +---------+----------+-----------+----------+------+----------+------------------+--------+ - """ - tax_21_incl = self.taxes['tax21'] - product1 = self.create_product( - name='Product 1', - category=self.categ_basic, - lst_price=12.10, - tax_ids=tax_21_incl.ids, - ) - product2 = self.create_product( - name='Product 2', - category=self.categ_basic, - lst_price=6.05, - tax_ids=tax_21_incl.ids, - ) - self.open_new_session() - self.env['pos.order'].sync_from_ui([self.create_ui_order_data([ - (product1, 1), - (product2, -1), - ])]) - self.pos_session.action_pos_session_validate() - - lines = self.pos_session.move_id.line_ids.sorted('balance') - self.assertEqual(2, len(lines.filtered(lambda l: l.tax_ids)), "Taxes should have been set on 2 lines") - self.assertEqual(4, len(lines.filtered(lambda l: l.tax_tag_ids)), "Tags should have been set on 4 lines") - self.assertRecordValues(lines, [ - # pylint: disable=bad-whitespace - {'account_id': self.sale_account.id, 'balance': -10.0, 'tax_ids': tax_21_incl.ids, 'tax_tag_ids': self.tax_tag_invoice_base.ids}, - {'account_id': self.tax_received_account.id, 'balance': -2.10, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_invoice_tax.ids}, - {'account_id': self.tax_received_account.id, 'balance': 1.05, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_refund_tax.ids}, - {'account_id': self.sale_account.id, 'balance': 5.00, 'tax_ids': tax_21_incl.ids, 'tax_tag_ids': self.tax_tag_refund_base.ids}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'balance': 6.05, 'tax_ids': [], 'tax_tag_ids': []}, - ]) - - def test_pos_create_account_move_round_globally(self): - """ Test for orders with global rounding enabled - - Orders - ====== - +---------+----------+-----------+----------+------+----------+------------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+-----------+----------+------+----------+------------------+--------+ - | order 1 | cash | no | product1 | 1 | 10.0 | 2.10 | 12.10 | - | | | | product2 | -1 | -5.0 | -1.05 | -6.05 | - +---------+----------+-----------+----------+------+----------+------------------+--------+ - """ - tax_21_incl = self.taxes['tax21'] - tax_21_incl.company_id.tax_calculation_rounding_method = 'round_globally' - - product1 = self.create_product( - name='Product 1', - category=self.categ_basic, - lst_price=12.10, - tax_ids=tax_21_incl.ids, - ) - product2 = self.create_product( - name='Product 2', - category=self.categ_basic, - lst_price=6.05, - tax_ids=tax_21_incl.ids, - ) - self.open_new_session() - self.env['pos.order'].sync_from_ui([self.create_ui_order_data([ - (product1, 1), - (product2, -1), - ])]) - self.pos_session.action_pos_session_validate() - - lines = self.pos_session.move_id.line_ids.sorted('balance') - self.assertEqual(2, len(lines.filtered(lambda l: l.tax_ids)), "Taxes should have been set on 2 lines") - self.assertEqual(4, len(lines.filtered(lambda l: l.tax_tag_ids)), "Tags should have been set on 4 lines") - self.assertRecordValues(lines, [ - # pylint: disable=bad-whitespace - {'account_id': self.sale_account.id, 'balance': -10.0, 'tax_ids': tax_21_incl.ids, 'tax_tag_ids': self.tax_tag_invoice_base.ids}, - {'account_id': self.tax_received_account.id, 'balance': -2.10, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_invoice_tax.ids}, - {'account_id': self.tax_received_account.id, 'balance': 1.05, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_refund_tax.ids}, - {'account_id': self.sale_account.id, 'balance': 5.00, 'tax_ids': tax_21_incl.ids, 'tax_tag_ids': self.tax_tag_refund_base.ids}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'balance': 6.05, 'tax_ids': [], 'tax_tag_ids': []}, - ]) - - def test_pos_create_correct_account_move_round_globally_discount(self): - """ Test for orders with global rounding enabled - - Orders - ====== - +---------+----------+------+----------+------+---------------------+-----------+---------------------------+---------+--------+--------+ - | order | payments | inv? | product | qty | original price unit | Discount | price unit after discount | untaxed | tax | total | - +---------+----------+------+----------+------+---------------------+-----------+---------------------------+---------+--------+--------+ - | order 1 | cash | no | product1 | 1 | 12.10 | 5% | 10.89 | 9.00 | 1.89 | 10.89 | - | | | | product2 | -1 | 6.05 | 5% | 5.45 | -4.50 | -0.95 | -5.445 | - +---------+----------+------+----------+------+---------------------+-----------+---------------------------+---------+--------+--------+ - """ - tax_21_incl = self.taxes['tax21'] - tax_21_incl.company_id.tax_calculation_rounding_method = 'round_globally' - - product1 = self.create_product( - name='Product 1', - category=self.categ_basic, - lst_price=12.10, - tax_ids=tax_21_incl.ids, - ) - product2 = self.create_product( - name='Product 2', - category=self.categ_basic, - lst_price=6.05, - tax_ids=tax_21_incl.ids, - ) - self.open_new_session() - self.env['pos.order'].sync_from_ui([self.create_ui_order_data([ - (product1, 1, 10), - (product2, -1, 10), - ])]) - self.pos_session.action_pos_session_validate() - - lines = self.pos_session.move_id.line_ids.sorted('balance') - - self.assertEqual(2, len(lines.filtered(lambda l: l.tax_ids)), "Taxes should have been set on 2 lines") - self.assertEqual(4, len(lines.filtered(lambda l: l.tax_tag_ids)), "Tags should have been set on 4 lines") - self.assertRecordValues(lines, [ - # pylint: disable=bad-whitespace - {'account_id': self.sale_account.id, 'balance': - 9.0, 'tax_ids': tax_21_incl.ids, 'tax_tag_ids': self.tax_tag_invoice_base.ids}, - {'account_id': self.tax_received_account.id, 'balance': -1.89, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_invoice_tax.ids}, - {'account_id': self.tax_received_account.id, 'balance': 0.95, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_refund_tax.ids}, - {'account_id': self.sale_account.id, 'balance': 4.5, 'tax_ids': tax_21_incl.ids, 'tax_tag_ids': self.tax_tag_refund_base.ids}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'balance': 5.44, 'tax_ids': [], 'tax_tag_ids': []}, - ]) - - def test_pos_create_correct_account_move_round_globally_discount_real_use_case(self): - """ Test for orders with global rounding enabled - - Orders - ====== - +---------+----------+------+----------+------+---------------------+-----------+---------------------------+---------+--------+--------+ - | order | payments | inv? | product | qty | original price unit | Discount | price unit after discount | untaxed | tax | total | - +---------+----------+------+----------+------+---------------------+-----------+---------------------------+---------+--------+--------+ - | order 1 | cash | no | product1 | 6 | 11.80 | 5% | 11.21 | 55.59 | 11.67 | 67.26 | - | | | | product2 | -6 | 15.30 | 5% | 14.535 | -72.07 | -15.14 | -87.21 | - +---------+----------+------+----------+------+---------------------+-----------+---------------------------+---------+--------+--------+ - """ - tax_21_incl = self.taxes['tax21'] - tax_21_incl.company_id.tax_calculation_rounding_method = 'round_globally' - - product1 = self.create_product( - name='Product 1', - category=self.categ_basic, - lst_price=11.80, - tax_ids=tax_21_incl.ids, - ) - product2 = self.create_product( - name='Product 2', - category=self.categ_basic, - lst_price=15.30, - tax_ids=tax_21_incl.ids, - ) - self.open_new_session() - self.env['pos.order'].sync_from_ui([self.create_ui_order_data([ - (product1, 6, 5), - (product2, -6, 5), - ])]) - self.pos_session.action_pos_session_validate() - - lines = self.pos_session.move_id.line_ids.sorted('balance') - - self.assertEqual(2, len(lines.filtered(lambda l: l.tax_ids)), "Taxes should have been set on 2 lines") - self.assertEqual(4, len(lines.filtered(lambda l: l.tax_tag_ids)), "Tags should have been set on 4 lines") - self.assertRecordValues(lines, [ - # pylint: disable=bad-whitespace - {'account_id': self.sale_account.id, 'balance': -55.59, 'tax_ids': tax_21_incl.ids, 'tax_tag_ids': self.tax_tag_invoice_base.ids}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'balance': -19.95, 'tax_ids': [], 'tax_tag_ids': []}, - {'account_id': self.tax_received_account.id, 'balance': -11.67, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_invoice_tax.ids}, - {'account_id': self.tax_received_account.id, 'balance': 15.14, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_refund_tax.ids}, - {'account_id': self.sale_account.id, 'balance': 72.07, 'tax_ids': tax_21_incl.ids, 'tax_tag_ids': self.tax_tag_refund_base.ids}, - ]) - - def test_fixed_tax_positive_qty(self): - - 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', - 'tag_ids': [(6, 0, self.tax_tag_invoice_base.ids)], - }), - (0, 0, { - 'repartition_type': 'tax', - 'account_id': self.tax_received_account.id, - 'tag_ids': [(6, 0, self.tax_tag_invoice_tax.ids)], - }), - ], - }) - - 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])], - }) - - self.open_new_session() - self.env['pos.order'].sync_from_ui([self.create_ui_order_data([ - (zero_amount_product, 1), - ])]) - self.pos_session.action_pos_session_validate() - - lines = self.pos_session.move_id.line_ids.sorted('balance') - - self.assertRecordValues(lines, [ - {'account_id': self.tax_received_account.id, 'balance': -1}, - {'account_id': self.sale_account.id, 'balance': 0}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'balance': 1}, - ]) - - def test_tax_is_used_when_in_transactions(self): - ''' Ensures that a tax is set to used when it is part of some transactions ''' - - # Call another test that uses product_1 - tax_pos = self.product1.taxes_id - self.assertFalse(tax_pos.is_used) - self.test_orders_no_invoiced() - tax_pos.invalidate_model(fnames=['is_used']) - self.assertTrue(tax_pos.is_used) - def test_pos_loaded_product_taxes_on_branch(self): """ Check loaded product taxes on branch company """ # create the following branch hierarchy: @@ -651,6 +143,7 @@ def test_pos_loaded_product_taxes_on_branch(self): xx_cash_journal = self.company_data['default_journal_cash'].copy({'company_id': branch_xx.id}) xx_cash_payment_method = self.env['pos.payment.method'].create({ 'name': 'XX Cash Payment', + 'type': 'cash', 'receivable_account_id': xx_account_receivable.id, 'journal_id': xx_cash_journal.id, 'company_id': branch_xx.id, @@ -732,25 +225,3 @@ def test_combo_product_variant_error(self): with self.assertRaises(UserError): with Form(self.variant_product.product_tmpl_id) as product: product.type = "combo" - - def test_tax_change_blocked_when_open_pos_session(self): - """Changing a POS sale tax must be blocked when a POS session is open""" - tax = self.taxes['tax7'] - - self.open_new_session() - self.assertEqual(self.pos_session.state, 'opened') - - self.env['pos.order'].sync_from_ui([self.create_ui_order_data([ - (self.product1, 1), - ])]) - - self.assertTrue(self.pos_session.order_ids) - self.assertTrue(self.env['pos.order.line'].search([ - ('order_id.session_id', '=', self.pos_session.id), - ('tax_ids', 'in', tax.ids), - ], limit=1)) - - with self.assertRaises(UserError): - tax.write({ - 'price_include_override': 'tax_included', - }) diff --git a/addons/point_of_sale/tests/test_pos_setup.py b/addons/point_of_sale/tests/test_pos_setup.py index b734a81d420116..d1c7eefb4fc385 100644 --- a/addons/point_of_sale/tests/test_pos_setup.py +++ b/addons/point_of_sale/tests/test_pos_setup.py @@ -75,7 +75,8 @@ def test_archive_used_journal(self): 'invoice_reference_type': 'invoice', 'invoice_reference_model': 'odoo' }) - payment_method = self.env['pos.payment.method'].create({'name': 'Lets Pay for Tests', 'journal_id': journal.id}) + payment_method = self.env['pos.payment.method'].create({ + 'name': 'Lets Pay for Tests', 'journal_id': journal.id, 'type': 'bank'}) self.basic_config.write({'payment_method_ids': [payment_method.id]}) journal.write({'pos_payment_method_ids': [payment_method.id]}) session = self.env['pos.session'].create( diff --git a/addons/point_of_sale/tests/test_pos_simple_invoiced_orders.py b/addons/point_of_sale/tests/test_pos_simple_invoiced_orders.py deleted file mode 100644 index 55af3dfd4d1582..00000000000000 --- a/addons/point_of_sale/tests/test_pos_simple_invoiced_orders.py +++ /dev/null @@ -1,790 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import odoo - -from odoo.addons.point_of_sale.tests.common import TestPoSCommon - - -@odoo.tests.tagged('post_install', '-at_install') -class TestPosSimpleInvoicedOrders(TestPoSCommon): - """ - Each test case only make a single **invoiced** order. - Name of each test corresponds to a sheet in: https://docs.google.com/spreadsheets/d/1mt2jRSDU7OONPBFjwyTcnhRjITQI8rGMLLQA5K3fAjo/edit?usp=sharing - """ - - def setUp(self): - super(TestPosSimpleInvoicedOrders, self).setUp() - self.config = self.basic_config - self.product100 = self.create_product('Product_100', self.categ_basic, 100, 50) - - def test_01b(self): - self._run_test({ - 'payment_methods': self.cash_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_pm1, 100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_02b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'journal_id': self.config.invoice_journal_id.id, - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 100), { - 'journal_id': self.config.journal_id.id, - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'journal_id': self.config.journal_id.id, - 'line_ids': [ - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((100, ), { - 'journal_id': self.bank_pm1.journal_id.id, - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_03b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.pay_later_pm, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'journal_id': self.config.invoice_journal_id.id, - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }, - 'payments': [], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': False, - 'cash_statement': [], - 'bank_payments': [], - }, - }) - - def test_04b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_split_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_split_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'journal_id': self.config.invoice_journal_id.id, - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_split_pm1, 100), { - 'journal_id': self.config.journal_id.id, - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'journal_id': self.config.journal_id.id, - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((100, ), { - 'journal_id': self.bank_split_pm1.journal_id.id, - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_05b(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_split_pm1, 100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_split_pm1, 100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_10b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_pm1, 200), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True, 'partially_reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_pm1, 200), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': False, 'partially_reconciled': True, 'amount_residual': -100}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False, 'amount_residual': 200}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_11b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_pm1, 200), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True, 'partially_reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 200), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': False, 'partially_reconciled': True, 'amount_residual': -100}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False, 'amount_residual': 200}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_12b(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_split_pm1, 200), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True, 'partially_reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_split_pm1, 200), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': False, 'partially_reconciled': True, 'amount_residual': -100}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False, 'amount_residual': 200}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_13b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_split_pm1, 200), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True, 'partially_reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_split_pm1, 200), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': False, 'partially_reconciled': True, 'amount_residual': -100}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False, 'amount_residual': 200}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_14b(self): - self._run_test({ - 'payment_methods': self.cash_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_pm1, 200), (self.cash_pm1, -100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_pm1, 200), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - ] - }), - ((self.cash_pm1, -100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_15b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_pm1, 200), (self.cash_pm1, -100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_pm1, 200), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - ] - }), - ((self.cash_pm1, -100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((-100, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_16b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_split_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_split_pm1, 200), (self.cash_pm1, -100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.bank_split_pm1, 200), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - ] - }), - ((self.cash_pm1, -100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((-100, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_17b(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_split_pm1, 200), (self.cash_split_pm1, -100)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }, - 'payments': [ - ((self.cash_split_pm1, 200), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - ] - }), - ((self.cash_split_pm1, -100), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }), - ((-100, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_18b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_pm1, 50), (self.pay_later_pm, 50)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False, 'amount_residual': -100}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False, 'partially_reconciled': True, 'amount_residual': 50}, - ] - }, - 'payments': [ - ((self.cash_pm1, 50), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 50, 'reconciled': True, 'partially_reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 50, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((50, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 50, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_19b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_pm1, 50), (self.pay_later_pm, 50)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False, 'amount_residual': -100}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False, 'partially_reconciled': True, 'amount_residual': 50}, - ] - }, - 'payments': [ - ((self.bank_pm1, 50), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 50, 'reconciled': True, 'partially_reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 50, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((50, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 50, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_20b(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_split_pm1, 50), (self.pay_later_pm, 50)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False, 'amount_residual': -100}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False, 'partially_reconciled': True, 'amount_residual': 50}, - ] - }, - 'payments': [ - ((self.bank_split_pm1, 50), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 50, 'reconciled': True, 'partially_reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 50, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((50, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 50, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_21b(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_split_pm1, 50), (self.pay_later_pm, 50)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': { - '00100-010-0001': { - 'invoice': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False, 'amount_residual': -100}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False, 'partially_reconciled': True, 'amount_residual': 50}, - ] - }, - 'payments': [ - ((self.cash_split_pm1, 50), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 50, 'reconciled': True, 'partially_reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': False}, - ] - }), - ], - } - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 50, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((50, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 50, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) diff --git a/addons/point_of_sale/tests/test_pos_simple_orders.py b/addons/point_of_sale/tests/test_pos_simple_orders.py deleted file mode 100644 index 943c485383d4bc..00000000000000 --- a/addons/point_of_sale/tests/test_pos_simple_orders.py +++ /dev/null @@ -1,589 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import odoo - -from odoo.addons.point_of_sale.tests.common import TestPoSCommon - - -@odoo.tests.tagged('post_install', '-at_install') -class TestPosSimpleOrders(TestPoSCommon): - """ - Each test case only make a single order. - Name of each test corresponds to a sheet in: https://docs.google.com/spreadsheets/d/1mt2jRSDU7OONPBFjwyTcnhRjITQI8rGMLLQA5K3fAjo/edit?usp=sharing - """ - - def setUp(self): - super(TestPosSimpleOrders, self).setUp() - self.config = self.basic_config - self.product100 = self.create_product('Product_100', self.categ_basic, 100, 50) - - def test_01(self): - self._run_test({ - 'payment_methods': self.cash_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_pm1, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_02(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_pm1, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_03(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.pay_later_pm, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - ], - }, - 'cash_statement': [], - 'bank_payments': [], - }, - }) - - def test_04(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_split_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_split_pm1, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_05(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_split_pm1, 100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_06(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [], 'payments': [(self.cash_pm1, 100), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_07(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [], 'payments': [(self.bank_pm1, 100), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_08(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [], 'payments': [(self.bank_split_pm1, 100), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_09(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [], 'payments': [(self.cash_split_pm1, 100), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_10(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_pm1, 200), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_11(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_pm1, 200), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_12(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_split_pm1, 200), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_13(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_split_pm1, 200), (self.pay_later_pm, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_14(self): - self._run_test({ - 'payment_methods': self.cash_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_pm1, 200), (self.cash_pm1, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((100, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_15(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_pm1, 200), (self.cash_pm1, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((-100, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_16(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_split_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_split_pm1, 200), (self.cash_pm1, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((-100, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_17(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_split_pm1, 200), (self.cash_split_pm1, -100)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((200, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 200, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 200, 'reconciled': True}, - ] - }), - ((-100, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 100, 'credit': 0, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_18(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_pm1, 50), (self.pay_later_pm, 50)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((50, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 50, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) - - def test_19(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_pm1, 50), (self.pay_later_pm, 50)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': False}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((50, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 50, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_20(self): - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.bank_split_pm1, 50), (self.pay_later_pm, 50)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': True}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': False}, - ], - }, - 'cash_statement': [], - 'bank_payments': [ - ((50, ), { - 'line_ids': [ - {'account_id': self.bank_split_pm1.outstanding_account_id.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 50, 'reconciled': True}, - ] - }) - ], - }, - }) - - def test_21(self): - self._run_test({ - 'payment_methods': self.cash_split_pm1 | self.pay_later_pm, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product100, 1)], 'payments': [(self.cash_split_pm1, 50), (self.pay_later_pm, 50)], 'customer': self.customer, 'is_invoiced': False, 'uuid': '00100-010-0001'}, - ], - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 100, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((50, ), { - 'line_ids': [ - {'account_id': self.cash_split_pm1.journal_id.default_account_id.id, 'partner_id': self.customer.id, 'debit': 50, 'credit': 0, 'reconciled': False}, - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 50, 'reconciled': True}, - ] - }) - ], - 'bank_payments': [], - }, - }) diff --git a/addons/point_of_sale/tests/test_pos_with_fiscal_position.py b/addons/point_of_sale/tests/test_pos_with_fiscal_position.py deleted file mode 100644 index 3b1173eaf79284..00000000000000 --- a/addons/point_of_sale/tests/test_pos_with_fiscal_position.py +++ /dev/null @@ -1,432 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import Command -import odoo -from odoo.addons.point_of_sale.tests.common import TestPoSCommon - -@odoo.tests.tagged('post_install', '-at_install') -class TestPoSWithFiscalPosition(TestPoSCommon): - """ Tests to pos orders with fiscal position. - - keywords/phrases: fiscal position - """ - - @classmethod - def setUpClass(cls): - super(TestPoSWithFiscalPosition, cls).setUpClass() - - cls.config = cls.basic_config - cls.company.tax_calculation_rounding_method = 'round_per_line' - - cls.new_tax_17 = cls.env['account.tax'].create({'name': 'New Tax 17%', 'amount': 17}) - cls.new_tax_17.invoice_repartition_line_ids.write({'account_id': cls.tax_received_account.id}) - - cls.fpos = cls._create_fiscal_position() - cls.fpos_no_tax_dest = cls._create_fiscal_position_no_tax_dest() - - cls.product1 = cls.create_product( - 'Product 1', - cls.categ_basic, - lst_price=10.99, - standard_price=5.0, - tax_ids=cls.taxes['tax7'].ids, - ) - cls.product2 = cls.create_product( - 'Product 2', - cls.categ_basic, - lst_price=19.99, - standard_price=10.0, - tax_ids=cls.taxes['tax10'].ids, - ) - cls.product3 = cls.create_product( - 'Product 3', - cls.categ_basic, - lst_price=30.99, - standard_price=15.0, - tax_ids=cls.taxes['tax7'].ids, - ) - - @classmethod - def _create_fiscal_position(cls): - fpos = cls.env['account.fiscal.position'].create({'name': 'Test Fiscal Position'}) - - account_fpos = cls.env['account.fiscal.position.account'].create({ - 'position_id': fpos.id, - 'account_src_id': cls.sale_account.id, - 'account_dest_id': cls.other_sale_account.id, - }) - fpos.write({ - 'account_ids': [(6, 0, account_fpos.ids)], - }) - cls.new_tax_17.write({ - 'fiscal_position_ids': [Command.link(fpos.id)], - 'original_tax_ids': [Command.link(cls.taxes['tax7'].id)], - }) - return fpos - - @classmethod - def _create_fiscal_position_no_tax_dest(cls): - fpos_no_tax_dest = cls.env['account.fiscal.position'].create({'name': 'Test Fiscal Position'}) - account_fpos = cls.env['account.fiscal.position.account'].create({ - 'position_id': fpos_no_tax_dest.id, - 'account_src_id': cls.sale_account.id, - 'account_dest_id': cls.other_sale_account.id, - }) - fpos_no_tax_dest.write({ - 'account_ids': [(6, 0, account_fpos.ids)], - }) - cls.env['account.tax'].create({ - 'name': 'Exempt', - 'amount': 0, - 'fiscal_position_ids': [Command.link(fpos_no_tax_dest.id)], - 'original_tax_ids': [Command.link(cls.taxes['tax7'].id)], - }) - return fpos_no_tax_dest - - def test_01_no_invoice_fpos(self): - """ orders without invoice - - Orders - ====== - +---------+----------+---------------+----------+-----+---------+-----------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+---------------+----------+-----+---------+-----------------+--------+ - | order 1 | cash | yes, customer | product1 | 10 | 109.90 | 18.68 [7%->17%] | 128.58 | - | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.90 | - | | | | product3 | 10 | 309.90 | 52.68 [7%->17%] | 362.58 | - +---------+----------+---------------+----------+-----+---------+-----------------+--------+ - | order 2 | cash | yes, customer | product1 | 5 | 54.95 | 9.34 [7%->17%] | 64.29 | - | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - +---------+----------+---------------+----------+-----+---------+-----------------+--------+ - | order 3 | bank | no | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - | | | | product3 | 5 | 154.95 | 10.85 [7%] | 165.8 | - +---------+----------+---------------+----------+-----+---------+-----------------+--------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | sale_account | -154.95 | (for the 7% base amount) - | sale_account | -90.86 | (for the 10% base amount) - | other_sale_account | -474.75 | (for the 17% base amount) - | other_sale_account | -272.59 | (for the 10% base amount) - | tax 17% | -80.70 | - | tax 10% | -36.35 | - | tax 7% | -10.85 | - | pos receivable bank | 265.75 | - | pos receivable cash | 855.30 | - +---------------------+---------+ - | Total balance | 0.0 | - +---------------------+---------+ - """ - - self.customer.write({'property_account_position_id': self.fpos.id}) - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 10), (self.product3, 10)], 'customer': self.customer, 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product1, 5), (self.product2, 5)], 'customer': self.customer, 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product2, 5), (self.product3, 5)], 'payments': [(self.bank_pm1, 265.75)], 'uuid': '00100-010-0003'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 80.70, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 36.35, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 10.85, 'reconciled': False}, - {'account_id': self.other_sale_account.id, 'partner_id': False, 'debit': 0, 'credit': 474.75, 'reconciled': False}, - {'account_id': self.other_sale_account.id, 'partner_id': False, 'debit': 0, 'credit': 272.59, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 90.86, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 154.95, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 265.75, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 855.30, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((855.30, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 855.30, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 855.30, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((265.75, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 265.75, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 265.75, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_02_no_invoice_fpos_no_tax_dest(self): - """ Customer with fiscal position that maps a tax to no tax. - - Orders - ====== - +---------+----------+---------------+----------+-----+---------+-------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+---------------+----------+-----+---------+-------------+--------+ - | order 1 | bank | yes, customer | product1 | 10 | 109.90 | 0 | 109.90 | - | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.90 | - | | | | product3 | 10 | 309.90 | 0 | 309.90 | - +---------+----------+---------------+----------+-----+---------+-------------+--------+ - | order 2 | cash | yes, customer | product1 | 5 | 54.95 | 0 | 54.95 | - | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - +---------+----------+---------------+----------+-----+---------+-------------+--------+ - | order 3 | bank | no | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - | | | | product3 | 5 | 154.95 | 10.85 [7%] | 165.80 | - +---------+----------+---------------+----------+-----+---------+-------------+--------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | sale_account | -154.95 | (for the 7% base amount) - | sale_account | -90.86 | (for the 10% base amount) - | other_sale_account | -272.59 | (for the 10% base amount) - | other_sale_account | -474.75 | (no tax) - | tax 10% | -36.35 | - | tax 7% | -10.85 | - | pos receivable bank | 885.45 | - | pos receivable cash | 154.9 | - +---------------------+---------+ - | Total balance | 0.0 | - +---------------------+---------+ - """ - - self.customer.write({'property_account_position_id': self.fpos_no_tax_dest.id}) - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 10), (self.product3, 10)], 'payments': [(self.bank_pm1, 619.7)], 'customer': self.customer, 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product1, 5), (self.product2, 5)], 'customer': self.customer, 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product2, 5), (self.product3, 5)], 'payments': [(self.bank_pm1, 265.75)], 'uuid': '00100-010-0003'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 36.35, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 10.85, 'reconciled': False}, - {'account_id': self.other_sale_account.id, 'partner_id': False, 'debit': 0, 'credit': 474.75, 'reconciled': False}, - {'account_id': self.other_sale_account.id, 'partner_id': False, 'debit': 0, 'credit': 272.59, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 90.86, 'reconciled': False}, - {'account_id': self.sales_account.id, 'partner_id': False, 'debit': 0, 'credit': 154.95, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 885.45, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 154.9, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((154.9, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 154.9, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 154.9, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((885.45, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 885.45, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 885.45, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_03_invoiced_fpos(self): - """ Invoice 2 orders. - - Orders - ====== - +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ - | order 1 | bank | yes, customer | product1 | 10 | 109.90 | 18.68 [7%->17%] | 128.58 | - | | | | product2 | 10 | 181.73 | 18.17 [10%] | 199.90 | - | | | | product3 | 10 | 309.90 | 52.68 [7%->17%] | 362.58 | - +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ - | order 2 | cash | no, customer | product1 | 5 | 54.95 | 9.34 [7%->17%] | 64.29 | - | | | | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ - | order 3 | cash | yes, other_customer | product2 | 5 | 90.86 | 9.09 [10%] | 99.95 | - | | | | product3 | 5 | 154.95 | 10.85 [7%] | 165.80 | - +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | other_sale_account | -54.95 | (for the 17% base amount) - | other_sale_account | -90.86 | (for the 10% base amount) - | tax 10% | -9.09 | - | tax 17% | -9.34 | - | pos receivable cash | 429.99 | - | pos receivable bank | 691.06 | - | receivable | -691.06 | - | other receivable | -265.75 | - +---------------------+---------+ - | Total balance | 0.0 | - +---------------------+---------+ - """ - - self.customer.write({'property_account_position_id': self.fpos.id}) - - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(3, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - invoiced_order_1 = self.pos_session.order_ids.filtered(lambda order: '00100-010-0001' in order.uuid) - invoiced_order_2 = self.pos_session.order_ids.filtered(lambda order: '00100-010-0003' in order.uuid) - - self.assertTrue(invoiced_order_1, msg='Invoiced order 1 should exist.') - self.assertTrue(invoiced_order_2, msg='Invoiced order 2 should exist.') - self.assertTrue(invoiced_order_1.account_move, msg='Invoiced order 1 should have invoice (account_move).') - self.assertTrue(invoiced_order_2.account_move, msg='Invoiced order 2 should have invoice (account_move).') - - self._run_test({ - 'payment_methods': self.cash_pm1 | self.bank_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10), (self.product2, 10), (self.product3, 10)], 'payments': [(self.bank_pm1, 691.06)], 'customer': self.customer, 'is_invoiced': True, 'uuid': '00100-010-0001'}, - {'pos_order_lines_ui_args': [(self.product1, 5), (self.product2, 5)], 'customer': self.customer, 'uuid': '00100-010-0002'}, - {'pos_order_lines_ui_args': [(self.product2, 5), (self.product3, 5)], 'customer': self.other_customer, 'is_invoiced': True, 'uuid': '00100-010-0003'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': { - '00100-010-0001': { - 'payments': [ - ((self.bank_pm1, 691.06), { - 'line_ids': [ - {'account_id': self.c1_receivable.id, 'partner_id': self.customer.id, 'debit': 0, 'credit': 691.06, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 691.06, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - '00100-010-0003': { - 'payments': [ - ((self.cash_pm1, 265.75), { - 'line_ids': [ - {'account_id': self.other_receivable_account.id, 'partner_id': self.other_customer.id, 'debit': 0, 'credit': 265.75, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 265.75, 'credit': 0, 'reconciled': False}, - ] - }), - ], - }, - }, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 9.34, 'reconciled': False}, - {'account_id': self.tax_received_account.id, 'partner_id': False, 'debit': 0, 'credit': 9.09, 'reconciled': False}, - {'account_id': self.other_sale_account.id, 'partner_id': False, 'debit': 0, 'credit': 54.95, 'reconciled': False}, - {'account_id': self.other_sale_account.id, 'partner_id': False, 'debit': 0, 'credit': 90.86, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 691.06, 'credit': 0, 'reconciled': True}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 429.99, 'credit': 0, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 691.06, 'reconciled': True}, - {'account_id': self.pos_receivable_account.id, 'partner_id': False, 'debit': 0, 'credit': 265.75, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((429.99, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 429.99, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 429.99, 'reconciled': True}, - ] - }), - ], - 'bank_payments': [ - ((691.06, ), { - 'line_ids': [ - {'account_id': self.bank_pm1.outstanding_account_id.id, 'partner_id': False, 'debit': 691.06, 'credit': 0, 'reconciled': False}, - {'account_id': self.bank_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 691.06, 'reconciled': True}, - ] - }), - ], - }, - }) - - def test_04_remove_tax_if_not_in_fp(self): - """ Tax a, with only fiscal position a set, should be removed if fiscal position b is set on order - - Orders - ====== - +---------+----------+---------------------+----------+-----+---------+------------------+--------+ - | order | payments | invoiced? | product | qty | untaxed | tax | total | - +---------+----------+---------------------+----------+-----+---------+------------------+--------+ - | order 1 | cash | yes, customer | product1 | 10 | 109.90 | 18.68 [7%->None] | 109.90 | - +---------+----------+---------------------+----------+-----+---------+-----------------+--------+ - - Expected Result - =============== - +---------------------+---------+ - | account | balance | - +---------------------+---------+ - | other_sale_account | -109.90 | - | pos receivable cash | 109.90 | - +---------------------+---------+ - | Total balance | 0.0 | - +---------------------+---------+ - """ - def _before_closing_cb(): - # check values before closing the session - self.assertEqual(1, self.pos_session.order_count) - orders_total = sum(order.amount_total for order in self.pos_session.order_ids) - self.assertAlmostEqual(orders_total, self.pos_session.total_payments_amount, msg='Total order amount should be equal to the total payment amount.') - - self.new_tax_17.original_tax_ids = None # cancel tax replacement - self.customer.property_account_position_id = self.fpos # enable applying fpos on order - dummy_fp = self.env['account.fiscal.position'].create({'name': 'Dummy FP'}) - self.taxes['tax7'].fiscal_position_ids |= dummy_fp # set a dummy fp on tax, as 'normal' taxes should have fp and and a tax without fp is never replaced - - self._run_test({ - 'payment_methods': self.cash_pm1, - 'orders': [ - {'pos_order_lines_ui_args': [(self.product1, 10)], 'customer': self.customer, 'uuid': '00100-010-0001'}, - ], - 'before_closing_cb': _before_closing_cb, - 'journal_entries_before_closing': {}, - 'journal_entries_after_closing': { - 'session_journal_entry': { - 'line_ids': [ - {'account_id': self.other_sale_account.id, 'partner_id': False, 'debit': 0, 'credit': 109.9, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 109.9, 'credit': 0, 'reconciled': True}, - ], - }, - 'cash_statement': [ - ((109.9, ), { - 'line_ids': [ - {'account_id': self.cash_pm1.journal_id.default_account_id.id, 'partner_id': False, 'debit': 109.9, 'credit': 0, 'reconciled': False}, - {'account_id': self.cash_pm1.receivable_account_id.id, 'partner_id': False, 'debit': 0, 'credit': 109.9, 'reconciled': True}, - ] - }), - ], - }, - }) diff --git a/addons/point_of_sale/tests/test_report_session.py b/addons/point_of_sale/tests/test_report_session.py index 6d1603920f2342..c4cf77a4b492a8 100644 --- a/addons/point_of_sale/tests/test_report_session.py +++ b/addons/point_of_sale/tests/test_report_session.py @@ -59,13 +59,10 @@ def test_report_session(self): self.make_payment(order, self.bank_split_pm1, 60) self.make_payment(order, self.bank_pm1, 50) - self.config.current_session_id.action_pos_session_closing_control(bank_payment_method_diffs={self.bank_split_pm1.id: 50, self.bank_pm1.id: 40}) + self.config.current_session_id.close_session_from_ui() # PoS Orders have negative IDs to avoid conflict, so reports[0] will correspond to the newest order report = self.env['report.point_of_sale.report_saledetails'].get_sale_details(session_ids=[session_id]) - split_payment_bank = [p for p in report['payments'] if p.get('id', 0) == self.bank_split_pm1.id] - self.assertEqual(split_payment_bank[0]['cash_moves'][0]['amount'], 50) - bank_payment = [p for p in report['payments'] if p.get('id', 0) == self.bank_pm1.id] # self.assertEqual(bank_payment[0]['cash_moves'][0]['amount'], 40) TODO WAN self.assertEqual(report['products_info']['total'], 100, "Total amount of products should be 100, as we want total without tax") self.assertEqual(report['products'][0]['products'][0]['base_amount'], 100, "Base amount of product should be 100, as we want price without tax") @@ -103,7 +100,7 @@ def test_report_session_2(self): order = self.env['pos.order'].create(order_info) self.make_payment(order, self.cash_pm1, 100) - self.config.current_session_id.action_pos_session_closing_control() + self.config.current_session_id.close_session_from_ui() self.config.open_ui() session_id_2 = self.config.current_session_id.id @@ -114,7 +111,7 @@ def test_report_session_2(self): order = self.env['pos.order'].create(order_info) self.make_payment(order, self.cash_pm1, 100) - self.config.current_session_id.action_pos_session_closing_control() + self.config.current_session_id.close_session_from_ui() report = self.env['report.point_of_sale.report_saledetails'].get_sale_details() for payment in report['payments']: @@ -131,17 +128,19 @@ def test_report_listing(self): cash_payment_method = self.env['pos.payment.method'].create({ 'name': 'Cash', + 'type': 'cash', 'receivable_account_id': self.company_data['default_account_receivable'].id, 'journal_id': self.company_data['default_journal_cash'].id, 'company_id': self.env.company.id, }) bank_payment_method = self.env['pos.payment.method'].create({ 'name': 'Bank', + 'type': 'bank', 'journal_id': self.company_data['default_journal_bank'].id, 'receivable_account_id': self.company_data['default_account_receivable'].id, 'company_id': self.env.company.id, }) - self.config.write({'payment_method_ids': [(4, bank_payment_method.id), (4, cash_payment_method.id)]}) + self.config.write({'payment_method_ids': [(5, 0), (4, bank_payment_method.id), (4, cash_payment_method.id)]}) self.open_new_session() session = self.pos_session @@ -227,7 +226,7 @@ def test_report_session_3(self): order_info['lines'][0][2]['qty'] = 59.7 order = self.env['pos.order'].create(order_info) self.make_payment(order, self.bank_pm1, 0) - self.config.current_session_id.action_pos_session_closing_control() + self.config.current_session_id.close_session_from_ui() report = self.env['report.point_of_sale.report_saledetails'].get_sale_details() self.assertEqual(report['products'][0]['products'][0]['quantity'], 74.6, "Quantity of product should be 74.6, as we want the sum of the quantity of the two orders") @@ -272,8 +271,8 @@ def test_report_bank_expected_different_than_counted(self): self.make_payment(order1, self.bank_pm1, 100) - self.config.current_session_id.action_pos_session_closing_control( - bank_payment_method_diffs={self.bank_pm1.id: -20}) + self.config.current_session_id.close_session_from_ui( + payment_method_closing={self.bank_pm1.id: 80}) report = self.env['report.point_of_sale.report_saledetails'].get_sale_details(session_ids=[session1_id]) self.assertEqual(report['payments'][1]['money_difference'], -20) @@ -306,7 +305,8 @@ def test_report_bank_expected_different_than_counted(self): self.make_payment(order2, self.bank_pm1, 100) - self.config.current_session_id.action_pos_session_closing_control(bank_payment_method_diffs={self.bank_pm1.id: -20}) + self.config.current_session_id.close_session_from_ui( + payment_method_closing={self.bank_pm1.id: 80}) report = self.env['report.point_of_sale.report_saledetails'].get_sale_details(session_ids=[session2_id]) self.assertEqual(report['payments'][1]['money_difference'], -20) @@ -350,7 +350,7 @@ def test_report_session_4(self): } order = self.env['pos.order'].create(order_info) self.make_payment(order, self.bank_pm1, 156.25) - self.config.current_session_id.action_pos_session_closing_control() + self.config.current_session_id.close_session_from_ui() report = self.env['report.point_of_sale.report_saledetails'].get_sale_details() self.assertEqual(report["taxes_info"]["base_amount"], 100, "Base amount should be equal to 100") @@ -360,7 +360,7 @@ def test_report_session_category_qty_round(self): quantities = [12.45, 88.21, 45.09, 7.33, 56.12, 92.84, 31.56, 19.47, 64.91, 5.02, 77.38, 41.65, 23.19, 99.72, 10.88] products = [self.create_product(f'Product {i}', self.categ_basic, 100) for i in range(len(quantities))] total = sum(quantities) - order_info = { + order_info = [{ 'company_id': self.env.company.id, 'session_id': session_id_1, 'partner_id': self.partner_a.id, @@ -380,15 +380,18 @@ def test_report_session_category_qty_round(self): 'amount_tax': 0.0, 'amount_return': 0.0, 'to_invoice': False, - } + } for _ in range(5)] - order = self.env['pos.order'].create(order_info) - self.make_payment(order, self.bank_pm1, total) - self.config.current_session_id.action_pos_session_closing_control() + for order_data in order_info: + order = self.env['pos.order'].create(order_data) + self.make_payment(order, self.bank_pm1, order.amount_total) + session = self.config.current_session_id + session.close_session_from_ui() + self.assertEqual(session.state, 'closed', "Session should be closed") report = self.env['report.point_of_sale.report_saledetails'].get_sale_details() - self.assertEqual(report['products'][0]['qty'], 675.82) - self.assertEqual(report['products'][0]['total'], 675.82) + self.assertAlmostEqual(report['products'][0]['qty'], 675.82 * 5) # The test create 5 orders + self.assertAlmostEqual(report['products'][0]['total'], 675.82 * 5) # The test create 5 orders def test_session_report_with_fp_and_discount(self): fiscal_position = self.env['account.fiscal.position'].create({ diff --git a/addons/point_of_sale/tests/test_res_config_settings.py b/addons/point_of_sale/tests/test_res_config_settings.py index 4b07e8196eb3b6..fab46f9ce01112 100644 --- a/addons/point_of_sale/tests/test_res_config_settings.py +++ b/addons/point_of_sale/tests/test_res_config_settings.py @@ -43,21 +43,19 @@ def test_properly_set_pos_config_x2many_fields(self): Command.create({ 'name': 'Bank 1', 'receivable_account_id': self.env.company.account_default_pos_receivable_account_id.id, - 'is_cash_count': False, - 'split_transactions': False, + 'type': 'bank', 'company_id': self.env.company.id, }), Command.create({ 'name': 'Bank 2', 'receivable_account_id': self.env.company.account_default_pos_receivable_account_id.id, - 'is_cash_count': False, - 'split_transactions': False, + 'type': 'bank', 'company_id': self.env.company.id, }), Command.create({ 'name': 'Cash', 'receivable_account_id': self.env.company.account_default_pos_receivable_account_id.id, - 'is_cash_count': True, + 'type': 'cash', 'company_id': self.env.company.id, }) ] diff --git a/addons/point_of_sale/views/point_of_sale_dashboard.xml b/addons/point_of_sale/views/point_of_sale_dashboard.xml index 6b6177c628b9b4..eb94d2474c2582 100644 --- a/addons/point_of_sale/views/point_of_sale_dashboard.xml +++ b/addons/point_of_sale/views/point_of_sale_dashboard.xml @@ -152,13 +152,13 @@
    Closing
    - +
    -
    +
    Balance
    - +
    diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml index 1dfbb068e501bc..0f3fad8714de34 100644 --- a/addons/point_of_sale/views/pos_config_view.xml +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -119,7 +119,7 @@ - + diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml index 0d4a462e7a2d89..e7c7da22ec5f49 100644 --- a/addons/point_of_sale/views/pos_order_view.xml +++ b/addons/point_of_sale/views/pos_order_view.xml @@ -8,7 +8,7 @@
    + invisible="not is_singly_invoiced"/> +
    -

    - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -82,9 +86,6 @@ - - - diff --git a/addons/point_of_sale/views/res_config_settings_views.xml b/addons/point_of_sale/views/res_config_settings_views.xml index 52c9c74c7f9cd1..f7891f81dabe43 100644 --- a/addons/point_of_sale/views/res_config_settings_views.xml +++ b/addons/point_of_sale/views/res_config_settings_views.xml @@ -112,7 +112,7 @@
    @@ -247,13 +247,12 @@