From 30e1deb87c89afd600d73469f670fad3f72030f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Tue, 4 Apr 2023 15:04:48 -0300 Subject: [PATCH 1/9] [IMP] pos_event_sale: copy event_ticket_id when cloning an Orderline --- pos_event_sale/static/src/js/models/Orderline.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pos_event_sale/static/src/js/models/Orderline.js b/pos_event_sale/static/src/js/models/Orderline.js index 1c81d8a683..8d0bf761e6 100644 --- a/pos_event_sale/static/src/js/models/Orderline.js +++ b/pos_event_sale/static/src/js/models/Orderline.js @@ -115,6 +115,14 @@ odoo.define("pos_event_sale.Orderline", function (require) { } return OrderlineSuper.get_full_product_name.apply(this, arguments); }, + /** + * @override + */ + clone: function () { + const res = OrderlineSuper.clone.apply(this, arguments); + res.event_ticket_id = this.event_ticket_id; + return res; + }, /** * @override */ From fd6d1ba07cb8ea087666a531da0865ab81f00305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Tue, 4 Apr 2023 15:05:02 -0300 Subject: [PATCH 2/9] [IMP] pos_event_sale_session: copy event_session_id when cloning an Orderline --- pos_event_sale_session/static/src/js/models/Orderline.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pos_event_sale_session/static/src/js/models/Orderline.js b/pos_event_sale_session/static/src/js/models/Orderline.js index 0e4ee992bb..e56ff7810b 100644 --- a/pos_event_sale_session/static/src/js/models/Orderline.js +++ b/pos_event_sale_session/static/src/js/models/Orderline.js @@ -49,6 +49,14 @@ odoo.define("pos_event_sale_session.Orderline", function (require) { } return OrderlineSuper.can_be_merged_with.apply(this, arguments); }, + /** + * @override + */ + clone: function () { + const res = OrderlineSuper.clone.apply(this, arguments); + res.event_session_id = this.event_session_id; + return res; + }, /** * @override */ From 4ca4dfdd4ca9b65abc99cc6b6b5a8a16921309fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Tue, 4 Apr 2023 16:56:04 -0300 Subject: [PATCH 3/9] [FIX] pos_event_sale: confirm only draft registrations When paying a `pos.order`, confirm only the `event.registration` that are in draft state. Don't attempt to confirm the ones that are already confirmed, nor the ones that might be cancelled. --- pos_event_sale/models/pos_order.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pos_event_sale/models/pos_order.py b/pos_event_sale/models/pos_order.py index 8e496cda03..4ee7921047 100644 --- a/pos_event_sale/models/pos_order.py +++ b/pos_event_sale/models/pos_order.py @@ -38,8 +38,9 @@ def action_open_event_registrations(self): def action_pos_order_paid(self): res = super().action_pos_order_paid() - self.event_registration_ids.action_confirm() - self.event_registration_ids._action_set_paid() + to_confirm = self.event_registration_ids.filtered(lambda r: r.state == "draft") + to_confirm.action_confirm() + to_confirm._action_set_paid() return res def action_pos_order_cancel(self): From 5fd85a1e5c1f15fd6b99b2a9728e198dba496da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Tue, 4 Apr 2023 16:52:35 -0300 Subject: [PATCH 4/9] [FIX] pos_event_sale: avoid cancelling done registrations If we cancel a `pos.order`, we should cancel the related `event.registration`(s) However, if the `event.registration` is already done, we should not cancel it as it would be already too late to do so. --- pos_event_sale/models/pos_order.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pos_event_sale/models/pos_order.py b/pos_event_sale/models/pos_order.py index 4ee7921047..0b0bbcf921 100644 --- a/pos_event_sale/models/pos_order.py +++ b/pos_event_sale/models/pos_order.py @@ -45,7 +45,8 @@ def action_pos_order_paid(self): def action_pos_order_cancel(self): res = super().action_pos_order_cancel() - self.event_registration_ids.action_cancel() + to_cancel = self.event_registration_ids.filtered(lambda r: r.state != "done") + to_cancel.action_cancel() return res def unlink(self): From 674a45b440b57139925ebb70893371733b774627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Tue, 4 Apr 2023 17:31:03 -0300 Subject: [PATCH 5/9] [FIX] pos_event_sale: disable auto_confirmation Registrations created from the PoS are not to be confirmed automatically. We confirm them when the `pos.order` is paid, but they are to remain in `draft` until then. --- pos_event_sale/models/event_registration.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pos_event_sale/models/event_registration.py b/pos_event_sale/models/event_registration.py index 23e1c6b15a..5129b74249 100644 --- a/pos_event_sale/models/event_registration.py +++ b/pos_event_sale/models/event_registration.py @@ -49,6 +49,13 @@ def _compute_payment_status(self): rec.payment_status = "not_paid" return res + def _check_auto_confirmation(self): + # OVERRIDE to disable auto confirmation for registrations created from + # PoS orders. We confirm them explicitly when the orders are paid. + if any(rec.pos_order_line_id for rec in self): + return False + return super()._check_auto_confirmation() + @api.model_create_multi def create(self, vals_list): # Override to post the origin-link message. From adc673b0bbba1128d526247258840170e74113c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Tue, 4 Apr 2023 17:41:47 -0300 Subject: [PATCH 6/9] [FIX] pos_event_sale: cancel refunded registrations when paid In the PoS, if the order is not paid, then it's not confirmed. If it's not confirmed, then we might as well ignore it. We shouldn't cancel any existing registrations until then. --- pos_event_sale/models/pos_order.py | 1 + pos_event_sale/models/pos_order_line.py | 1 - pos_event_sale/tests/test_frontend.py | 13 ++++++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pos_event_sale/models/pos_order.py b/pos_event_sale/models/pos_order.py index 0b0bbcf921..10dc657c9c 100644 --- a/pos_event_sale/models/pos_order.py +++ b/pos_event_sale/models/pos_order.py @@ -38,6 +38,7 @@ def action_open_event_registrations(self): def action_pos_order_paid(self): res = super().action_pos_order_paid() + self.lines._cancel_refunded_event_registrations() to_confirm = self.event_registration_ids.filtered(lambda r: r.state == "draft") to_confirm.action_confirm() to_confirm._action_set_paid() diff --git a/pos_event_sale/models/pos_order_line.py b/pos_event_sale/models/pos_order_line.py index 254ac65ee1..d0d5c11d7e 100644 --- a/pos_event_sale/models/pos_order_line.py +++ b/pos_event_sale/models/pos_order_line.py @@ -34,7 +34,6 @@ class PosOrderLine(models.Model): def create(self, vals_list): records = super().create(vals_list) records._create_event_registrations() - records._cancel_refunded_event_registrations() return records def _prepare_event_registration_vals(self): diff --git a/pos_event_sale/tests/test_frontend.py b/pos_event_sale/tests/test_frontend.py index 997748f2df..8faa9758e4 100644 --- a/pos_event_sale/tests/test_frontend.py +++ b/pos_event_sale/tests/test_frontend.py @@ -75,7 +75,18 @@ def test_pos_event_sale_basic_tour(self): action = self.event.action_view_pos_orders() self.assertEqual(self.env["pos.order"].search(action["domain"]), pos_order) # Refund the order - pos_order.refund() + refund = self.env["pos.order"].browse(pos_order.refund()["res_id"]) + # Pay the refund + self.env["pos.make.payment"].with_context( + active_ids=refund.ids, + active_id=refund.id, + active_model=refund._name, + ).create( + { + "payment_method_id": self.main_pos_config.payment_method_ids[0].id, + "amount": refund.amount_total, + } + ).check() for reg in pos_order.event_registration_ids: self.assertEqual(reg.state, "cancel") From c08ec6b03c22f27596508e2d932093dfec7c00f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Tue, 4 Apr 2023 18:03:25 -0300 Subject: [PATCH 7/9] [IMP] pos_event_sale: allow to negate lines Negated lines are particularly useful with PoS modules that disable the removal of pos orderlines through the use of ``disallowLineQuantityChange``. This is the case of `l10n_fr_pos_cert` module, for example. But there are other core modules doing it, too. In this case, added order lines can't be removed. Instead, the cashier is encouraged to add a negative to 'cancel' the original one. e.g.: Adds a line with 10 tickets. Then, adds a line with -10 tickets. This should be almost the same as removing the first line: no actual registration should be confirmed in the end, as we're reverting this sale. --- pos_event_sale/models/pos_order.py | 1 + pos_event_sale/models/pos_order_line.py | 41 +++++++++++++++++++ .../static/src/js/Screens/PaymentScreen.js | 5 ++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pos_event_sale/models/pos_order.py b/pos_event_sale/models/pos_order.py index 10dc657c9c..778a2b41ab 100644 --- a/pos_event_sale/models/pos_order.py +++ b/pos_event_sale/models/pos_order.py @@ -38,6 +38,7 @@ def action_open_event_registrations(self): def action_pos_order_paid(self): res = super().action_pos_order_paid() + self.lines._cancel_negated_event_registrations() self.lines._cancel_refunded_event_registrations() to_confirm = self.event_registration_ids.filtered(lambda r: r.state == "draft") to_confirm.action_confirm() diff --git a/pos_event_sale/models/pos_order_line.py b/pos_event_sale/models/pos_order_line.py index d0d5c11d7e..44e6404044 100644 --- a/pos_event_sale/models/pos_order_line.py +++ b/pos_event_sale/models/pos_order_line.py @@ -91,6 +91,47 @@ def _cancel_refunded_event_registrations(self): body=_("Refunded on %s", line.order_id.session_id.name) ) + def _find_event_registrations_to_negate(self): + """Find the registrations that could be negated by this order line""" + self.ensure_one() + return self.order_id.event_registration_ids.filtered( + lambda r: ( + r.event_ticket_id + and r.event_ticket_id == self.event_ticket_id + and r.state == "draft" + ) + ) + + def _cancel_negated_event_registrations(self): + """Cancel negated event registrations + + This happens when the order contains a negative event line + that is not a refund of another order's line. + + For example: + + * Line 1: 10 tickets for event A + * Line 2: -5 tickets for event A + + In this case, we need to cancel 5 registrations of the first + order line, as they are negated by the second line. + + These registrations are never confirmed. They go from `draft` + to `cancel` directly. + """ + to_process = self.filtered( + lambda line: ( + line.event_ticket_id + and int(line.qty) < 0 + and not line.refunded_orderline_id + ) + ) + for line in to_process: + qty_to_cancel = max(0, -int(line.qty)) + registrations = line._find_event_registrations_to_negate() + registrations = registrations[-qty_to_cancel:] + registrations.action_cancel() + def _export_for_ui(self, orderline): # OVERRIDE to add event_ticket_id res = super()._export_for_ui(orderline) diff --git a/pos_event_sale/static/src/js/Screens/PaymentScreen.js b/pos_event_sale/static/src/js/Screens/PaymentScreen.js index b8309ab132..cb11c1eaa4 100644 --- a/pos_event_sale/static/src/js/Screens/PaymentScreen.js +++ b/pos_event_sale/static/src/js/Screens/PaymentScreen.js @@ -17,7 +17,10 @@ odoo.define("pos_event_sale.PaymentScreen", function (require) { order.event_registrations = await this.rpc({ model: "event.registration", method: "search_read", - domain: [["pos_order_id", "in", server_ids]], + domain: [ + ["pos_order_id", "in", server_ids], + ["state", "=", "open"], + ], kwargs: {context: session.user_context}, }); } From 06be7c11f4153f8d1c62c55634d4376fa38f9158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Tue, 4 Apr 2023 18:03:45 -0300 Subject: [PATCH 8/9] [IMP] pos_event_sale_session: allow to negate lines --- pos_event_sale_session/models/pos_order_line.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pos_event_sale_session/models/pos_order_line.py b/pos_event_sale_session/models/pos_order_line.py index ba7afa193b..059cf37e18 100644 --- a/pos_event_sale_session/models/pos_order_line.py +++ b/pos_event_sale_session/models/pos_order_line.py @@ -26,6 +26,14 @@ def _prepare_refund_data(self, refund_order, PosOrderLineLot): res["event_session_id"] = self.event_session_id.id return res + def _find_event_registrations_to_negate(self): + # OVERRIDE to match also by event_session_id + return ( + super() + ._find_event_registrations_to_negate() + .filtered(lambda r: r.session_id == self.event_session_id) + ) + def _export_for_ui(self, orderline): # OVERRIDE to add event_session_id res = super()._export_for_ui(orderline) From 097acf2b6319559379451010186b3efa314fa54d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Wed, 5 Apr 2023 17:13:35 -0300 Subject: [PATCH 9/9] [IMP] pos_event_sale: refactor and add unit tests Moved some ORM tests to `tests_backend`, that will execute faster than tours. Added unit tests to cover more backend scenarios. --- pos_event_sale/tests/__init__.py | 1 + pos_event_sale/tests/common.py | 228 ++++++++++++++++++++++++++ pos_event_sale/tests/test_backend.py | 89 ++++++++++ pos_event_sale/tests/test_frontend.py | 84 +--------- 4 files changed, 325 insertions(+), 77 deletions(-) create mode 100644 pos_event_sale/tests/common.py create mode 100644 pos_event_sale/tests/test_backend.py diff --git a/pos_event_sale/tests/__init__.py b/pos_event_sale/tests/__init__.py index ab211c0007..a97e1da728 100644 --- a/pos_event_sale/tests/__init__.py +++ b/pos_event_sale/tests/__init__.py @@ -1 +1,2 @@ +from . import test_backend from . import test_frontend diff --git a/pos_event_sale/tests/common.py b/pos_event_sale/tests/common.py new file mode 100644 index 0000000000..3c56fca8d6 --- /dev/null +++ b/pos_event_sale/tests/common.py @@ -0,0 +1,228 @@ +# Copyright 2023 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from random import randint + +from odoo import fields + +from odoo.addons.point_of_sale.tests.common import TestPoSCommon +from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon + + +class PosEventMixin: + @classmethod + def setUpData(cls): + # Configure product + cls.product_event = cls.env.ref("event_sale.product_product_event") + cls.product_event.active = True + cls.product_event.available_in_pos = True + # Create event + cls.event = cls.env["event.event"].create( + { + "name": "Les Misérables", + "event_type_id": cls.env.ref("event.event_type_0").id, + "date_begin": fields.Datetime.start_of(fields.Datetime.now(), "day"), + "date_end": fields.Datetime.end_of(fields.Datetime.now(), "day"), + "stage_id": cls.env.ref("event.event_stage_booked").id, + "seats_limited": True, + "seats_max": 10, + } + ) + cls.ticket_kids = cls.env["event.event.ticket"].create( + { + "name": "Kids", + "product_id": cls.product_event.id, + "event_id": cls.event.id, + "price": 0.0, + "seats_limited": True, + "seats_max": 5, + } + ) + cls.ticket_regular = cls.env["event.event.ticket"].create( + { + "name": "Standard", + "product_id": cls.product_event.id, + "event_id": cls.event.id, + "price": 15.0, + } + ) + + +class TestPoSEventCommon(TestPoSCommon, PosEventMixin): + @classmethod + def setUpClass(cls, **kwargs): + super().setUpClass(**kwargs) + cls.setUpData() + cls.env.user.groups_id += cls.env.ref("event.group_event_user") + cls.basic_config.iface_event_sale = True + cls.config = cls.basic_config + # Open session + cls.config.open_session_cb(check_coa=False) + cls.pos_session = cls.config.current_session_id + cls.currency = cls.pos_session.currency_id + cls.pricelist = cls.pos_session.config_id.pricelist_id + cls.pos_session.set_cashbox_pos(0.0, None) + # Used to generate unique order ids + cls._nextId = 0 + + @classmethod + def _create_order_line_data(cls, product=None, quantity=1.0, discount=0.0, fp=None): + cls._nextId += 1 + price_unit = cls.pricelist.get_product_price(product, quantity, False) + tax_ids = fp.map_tax(product.taxes_id) if fp else product.taxes_id + price_unit_after_discount = price_unit * (1 - discount / 100.0) + tax_values = ( + tax_ids.compute_all(price_unit_after_discount, cls.currency, quantity) + if tax_ids + else { + "total_excluded": price_unit * quantity, + "total_included": price_unit * quantity, + } + ) + return { + "discount": discount, + "id": cls._nextId, + "pack_lot_ids": [], + "price_unit": price_unit, + "product_id": product.id, + "price_subtotal": tax_values["total_excluded"], + "price_subtotal_incl": tax_values["total_included"], + "qty": quantity, + "tax_ids": [(6, 0, tax_ids.ids)], + } + + @classmethod + def _create_event_order_line_data( + cls, ticket=None, quantity=1.0, discount=0.0, fp=None + ): + cls._nextId += 1 + product = ticket.product_id + product_lst_price = product.lst_price + product_price = cls.pricelist.get_product_price(product, quantity, False) + price_unit = product_price / product_lst_price * ticket.price + tax_ids = ( + fp.map_tax(ticket.product_id.taxes_id) if fp else ticket.product_id.taxes_id + ) + price_unit_after_discount = price_unit * (1 - discount / 100.0) + tax_values = ( + tax_ids.compute_all(price_unit_after_discount, cls.currency, quantity) + if tax_ids + else { + "total_excluded": price_unit * quantity, + "total_included": price_unit * quantity, + } + ) + return { + "discount": discount, + "id": cls._nextId, + "pack_lot_ids": [], + "price_unit": price_unit, + "product_id": ticket.product_id.id, + "price_subtotal": tax_values["total_excluded"], + "price_subtotal_incl": tax_values["total_included"], + "qty": quantity, + "tax_ids": [(6, 0, tax_ids.ids)], + "event_ticket_id": ticket.id, + } + + @classmethod + def _create_random_uid(cls): + return "%05d-%03d-%04d" % (randint(1, 99999), randint(1, 999), randint(1, 9999)) + + @classmethod + def _create_order_data( + cls, + lines=None, + event_lines=None, + partner=False, + is_invoiced=False, + payments=None, + uid=None, + ): + """Create a dictionary mocking data created by the frontend""" + default_fiscal_position = cls.config.default_fiscal_position_id + fiscal_position = ( + partner.property_account_position_id if partner else default_fiscal_position + ) + uid = uid or cls._create_random_uid() + # Lines + order_lines = [] + if lines: + order_lines.extend( + [ + cls._create_order_line_data(**line, fp=fiscal_position) + for line in lines + ] + ) + if event_lines: + order_lines.extend( + [ + cls._create_event_order_line_data(**line, fp=fiscal_position) + for line in event_lines + ] + ) + # Payments + total_amount_incl = sum(line["price_subtotal_incl"] for line in order_lines) + if payments is None: + default_cash_pm = cls.config.payment_method_ids.filtered( + lambda pm: pm.is_cash_count + )[:1] + if not default_cash_pm: + raise Exception( + "There should be a cash payment method set in the pos.config." + ) + payments = [ + dict( + amount=total_amount_incl, + name=fields.Datetime.now(), + payment_method_id=default_cash_pm.id, + ) + ] + else: + payments = [ + dict(amount=amount, name=fields.Datetime.now(), payment_method_id=pm.id) + for pm, amount in payments + ] + # Order data + total_amount_base = sum(line["price_subtotal"] for line in order_lines) + return { + "data": { + "amount_paid": sum(payment["amount"] for payment in payments), + "amount_return": 0, + "amount_tax": total_amount_incl - total_amount_base, + "amount_total": total_amount_incl, + "creation_date": fields.Datetime.to_string(fields.Datetime.now()), + "fiscal_position_id": fiscal_position.id, + "pricelist_id": cls.config.pricelist_id.id, + "lines": [(0, 0, line) for line in order_lines], + "name": "Order %s" % uid, + "partner_id": partner and partner.id, + "pos_session_id": cls.pos_session.id, + "sequence_number": 2, + "statement_ids": [(0, 0, payment) for payment in payments], + "uid": uid, + "user_id": cls.env.user.id, + "to_invoice": is_invoiced, + }, + "id": uid, + "to_invoice": is_invoiced, + } + + @classmethod + def _create_from_ui(cls, order_list, draft=False): + if not isinstance(order_list, (list, tuple)): + order_list = [order_list] + order_data_list = [cls._create_order_data(**order) for order in order_list] + res = cls.env["pos.order"].create_from_ui(order_data_list, draft=draft) + order_ids = [order["id"] for order in res] + return cls.env["pos.order"].browse(order_ids) + + +class TestPoSEventHttpCommon(TestPointOfSaleHttpCommon, PosEventMixin): + @classmethod + def setUpClass(cls, **kwargs): + super().setUpClass(**kwargs) + cls.setUpData() + cls.env.user.groups_id += cls.env.ref("event.group_event_user") + cls.main_pos_config.iface_event_sale = True + cls.main_pos_config.open_session_cb(check_coa=False) diff --git a/pos_event_sale/tests/test_backend.py b/pos_event_sale/tests/test_backend.py new file mode 100644 index 0000000000..1497a162c9 --- /dev/null +++ b/pos_event_sale/tests/test_backend.py @@ -0,0 +1,89 @@ +# Copyright 2023 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import tagged + +from .common import TestPoSEventCommon + + +@tagged("post_install", "-at_install") +class TestPoSEvent(TestPoSEventCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.order = cls._create_from_ui( + dict( + event_lines=[ + dict(ticket=cls.ticket_kids, quantity=2), + dict(ticket=cls.ticket_regular, quantity=1), + ] + ) + ) + + def test_action_open_registrations(self): + action = self.order.action_open_event_registrations() + self.assertEqual(action["type"], "ir.actions.act_window") + + def test_event_pos_price_subtotal(self): + self.assertEqual(self.event.pos_price_subtotal, self.order.amount_total) + + def test_event_action_view_pos_orders(self): + action = self.event.action_view_pos_orders() + self.assertEqual(self.env["pos.order"].search(action["domain"]), self.order) + + def test_order_refund(self): + refund = self.env["pos.order"].browse(self.order.refund()["res_id"]) + self.env["pos.make.payment"].with_context( + active_ids=refund.ids, + active_id=refund.id, + active_model=refund._name, + ).create( + { + "payment_method_id": self.config.payment_method_ids[0].id, + "amount": refund.amount_total, + } + ).check() + self.assertTrue( + all(reg.state == "cancel" for reg in self.order.event_registration_ids) + ) + + def test_order_cancel(self): + done_registrations = self.order.event_registration_ids[-2:] + open_registrations = self.order.event_registration_ids - done_registrations + done_registrations.action_set_done() + self.order.action_pos_order_cancel() + self.assertTrue( + all(reg.state == "cancel" for reg in open_registrations), + "Open registrations should be cancelled with the order", + ) + self.assertTrue( + all(reg.state == "done" for reg in done_registrations), + "Done registrations should remain done", + ) + + def test_order_with_negated_registrations(self): + order = self._create_from_ui( + dict( + event_lines=[ + dict(ticket=self.ticket_kids, quantity=2), + dict(ticket=self.ticket_kids, quantity=-2), + dict(ticket=self.ticket_regular, quantity=1), + ] + ) + ) + kids_registrations = order.event_registration_ids.filtered( + lambda r: r.event_ticket_id == self.ticket_kids + ) + self.assertEqual(len(kids_registrations), 2) + self.assertTrue( + all(reg.state == "cancel" for reg in kids_registrations), + "Kids registrations should be cancelled (negated)", + ) + regular_registrations = order.event_registration_ids.filtered( + lambda r: r.event_ticket_id == self.ticket_regular + ) + self.assertEqual(len(regular_registrations), 1) + self.assertTrue( + all(reg.state == "open" for reg in regular_registrations), + "Regular registrations should be confirmed", + ) diff --git a/pos_event_sale/tests/test_frontend.py b/pos_event_sale/tests/test_frontend.py index 8faa9758e4..213f7d2f6e 100644 --- a/pos_event_sale/tests/test_frontend.py +++ b/pos_event_sale/tests/test_frontend.py @@ -2,98 +2,28 @@ # @author Iván Todorovich # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from datetime import datetime - -from odoo import fields from odoo.tests import tagged -from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon +from .common import TestPoSEventHttpCommon @tagged("post_install", "-at_install") -class TestUi(TestPointOfSaleHttpCommon): - @classmethod - def setUpClass(cls, **kwargs): - super().setUpClass(**kwargs) - cls.env.user.groups_id += cls.env.ref("event.group_event_user") - cls.main_pos_config.iface_event_sale = True - # Configure product - cls.product_event = cls.env.ref("event_sale.product_product_event") - cls.product_event.active = True - cls.product_event.available_in_pos = True - # Create event - cls.event = cls.env["event.event"].create( - { - "name": "Les Misérables", - "event_type_id": cls.env.ref("event.event_type_0").id, - "date_begin": datetime.combine( - fields.Date.today(), datetime.min.time() - ), - "date_end": datetime.combine(fields.Date.today(), datetime.max.time()), - "stage_id": cls.env.ref("event.event_stage_booked").id, - "seats_limited": True, - "seats_max": 10, - } - ) - cls.ticket_kids = cls.env["event.event.ticket"].create( - { - "name": "Kids", - "product_id": cls.product_event.id, - "event_id": cls.event.id, - "price": 0.0, - "seats_limited": True, - "seats_max": 5, - } - ) - cls.ticket_regular = cls.env["event.event.ticket"].create( - { - "name": "Standard", - "product_id": cls.product_event.id, - "event_id": cls.event.id, - "price": 15.0, - } - ) - +class TestPoSEventHttp(TestPoSEventHttpCommon): def test_pos_event_sale_basic_tour(self): - self.main_pos_config.open_session_cb(check_coa=False) self.start_tour( f"/pos/ui?config_id={self.main_pos_config.id}", "EventSaleTour", login="accountman", ) - # Check POS Order (last created order) - pos_order = self.env["pos.order"].search([], order="id desc", limit=1) - # Check registrations - self.assertEqual(pos_order.event_registrations_count, 3) - for reg in pos_order.event_registration_ids: - self.assertEqual(reg.state, "open") - # Check action open registrations - action = pos_order.action_open_event_registrations() - self.assertEqual(action["type"], "ir.actions.act_window") - # Total sold amount for the event - self.assertEqual(self.event.pos_price_subtotal, 30.0) - action = self.event.action_view_pos_orders() - self.assertEqual(self.env["pos.order"].search(action["domain"]), pos_order) - # Refund the order - refund = self.env["pos.order"].browse(pos_order.refund()["res_id"]) - # Pay the refund - self.env["pos.make.payment"].with_context( - active_ids=refund.ids, - active_id=refund.id, - active_model=refund._name, - ).create( - { - "payment_method_id": self.main_pos_config.payment_method_ids[0].id, - "amount": refund.amount_total, - } - ).check() - for reg in pos_order.event_registration_ids: - self.assertEqual(reg.state, "cancel") + order = self.env["pos.order"].search([], order="id desc", limit=1) + self.assertTrue( + all(reg.state == "open" for reg in order.event_registration_ids), + "Registrations should be confirmed", + ) def test_pos_event_sale_availability_tour(self): self.event.seats_max = 5 self.ticket_kids.seats_max = 3 - self.main_pos_config.open_session_cb(check_coa=False) self.start_tour( f"/pos/ui?config_id={self.main_pos_config.id}", "EventSaleAvailabilityTour",