Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
7 changes: 7 additions & 0 deletions pos_event_sale/models/event_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 7 additions & 3 deletions pos_event_sale/models/pos_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ 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()
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()
to_confirm._action_set_paid()
return res

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):
Expand Down
42 changes: 41 additions & 1 deletion pos_event_sale/models/pos_order_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -92,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)
Expand Down
5 changes: 4 additions & 1 deletion pos_event_sale/static/src/js/Screens/PaymentScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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},
});
}
Expand Down
8 changes: 8 additions & 0 deletions pos_event_sale/static/src/js/models/Orderline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
1 change: 1 addition & 0 deletions pos_event_sale/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import test_backend
from . import test_frontend
228 changes: 228 additions & 0 deletions pos_event_sale/tests/common.py
Original file line number Diff line number Diff line change
@@ -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)
Loading