601 lines
26 KiB
Python
601 lines
26 KiB
Python
from odoo import models, fields, api, Command
|
|
from collections import defaultdict
|
|
from itertools import groupby
|
|
import base64
|
|
import uuid
|
|
import pytz
|
|
|
|
|
|
class PosSession(models.Model):
|
|
_inherit = "pos.session"
|
|
|
|
l10n_be_users_clocked_ids = fields.Many2many(
|
|
"res.users",
|
|
"users_session_clocking_info",
|
|
string="Clocked In Users",
|
|
)
|
|
pos_clock_in_out_ids = fields.One2many('pos.clock.in.out', 'session_id', string='POS Clock In/Out')
|
|
|
|
l10n_be_N_event_counter = fields.Integer()
|
|
l10n_be_N_event_amount = fields.Monetary()
|
|
l10n_be_NR_event_counter = fields.Integer()
|
|
l10n_be_NR_event_amount = fields.Monetary()
|
|
l10n_be_P_event_counter = fields.Integer()
|
|
l10n_be_P_event_amount = fields.Monetary()
|
|
l10n_be_I_event_counter = fields.Integer()
|
|
l10n_be_I_event_amount = fields.Monetary()
|
|
l10n_be_T_event_counter = fields.Integer()
|
|
l10n_be_T_event_amount = fields.Monetary()
|
|
|
|
l10n_be_sign_drawer_open_amount = fields.Integer()
|
|
|
|
l10n_be_correction_qty_count = fields.Integer()
|
|
l10n_be_correction_ticket_count = fields.Integer()
|
|
l10n_be_correction_amount = fields.Monetary()
|
|
l10n_be_price_change_qty_count = fields.Integer()
|
|
l10n_be_price_change_ticket_count = fields.Integer()
|
|
l10n_be_price_change_amount = fields.Monetary()
|
|
|
|
l10n_be_turnover_z_report_fdm_ref = fields.Json(string="Turnover Z Report FDM Reference")
|
|
l10n_be_user_z_report_fdm_ref = fields.Json(string="User Z Report FDM Reference")
|
|
|
|
booking_period_id = fields.Char(string='Booking Period ID', default=lambda self: str(uuid.uuid4()), readonly=True, copy=False)
|
|
|
|
first_fdm_ref = fields.Json(string="First FDM Reference", readonly=True, copy=False)
|
|
last_fdm_ref = fields.Json(string="Last FDM Reference", readonly=True, copy=False)
|
|
|
|
old_pos_blackbox_be_data = fields.Json(string="Old POS Blackbox BE Data", readonly=True, copy=False)
|
|
|
|
# TODO-manv: in master store this field
|
|
use_blackbox = fields.Boolean(string="Uses Blackbox", compute="_compute_use_blackbox")
|
|
|
|
@api.depends("pos_clock_in_out_ids")
|
|
def _compute_use_blackbox(self):
|
|
for session in self:
|
|
session.use_blackbox = bool(session.pos_clock_in_out_ids)
|
|
|
|
@api.model
|
|
def _load_pos_data_models(self, config_id):
|
|
return super()._load_pos_data_models(config_id) + ['pos.blackbox.be', 'ir.ui.view']
|
|
|
|
@api.model
|
|
def _load_pos_data_fields(self, config_id):
|
|
return super()._load_pos_data_fields(config_id) + ['l10n_be_users_clocked_ids', 'booking_period_id']
|
|
|
|
def is_session_closable(self, bank_payment_method_diffs=None):
|
|
self.ensure_one()
|
|
result = self._cannot_close_session(dict(bank_payment_method_diffs or []))
|
|
return not bool(result)
|
|
|
|
def set_work_in_out_cashier(self, cashier_id, action="in"):
|
|
self.ensure_one()
|
|
cashier = self.env['res.users'].browse(cashier_id)
|
|
clocked_ids = self.l10n_be_users_clocked_ids
|
|
|
|
if action == 'in' and cashier not in clocked_ids:
|
|
self.l10n_be_users_clocked_ids |= cashier
|
|
self._create_clock_events(cashier, 'in')
|
|
elif action == 'out' and cashier in clocked_ids:
|
|
self.l10n_be_users_clocked_ids -= cashier
|
|
self._create_clock_events(cashier, 'out')
|
|
|
|
def work_out_all_cashiers(self):
|
|
self.ensure_one()
|
|
clocked_ids = self.l10n_be_users_clocked_ids
|
|
self._create_clock_events(clocked_ids, 'out')
|
|
self.l10n_be_users_clocked_ids = [Command.clear()]
|
|
return clocked_ids.mapped('l10n_be_insz_or_bis_number')
|
|
|
|
def increment_l10n_be_counters(self, event_label, amount):
|
|
self.ensure_one()
|
|
if event_label == "N":
|
|
self.l10n_be_N_event_counter += 1
|
|
self.l10n_be_N_event_amount += self.currency_id.round(amount)
|
|
if amount < 0:
|
|
self.l10n_be_NR_event_counter += 1
|
|
self.l10n_be_NR_event_amount += self.currency_id.round(amount)
|
|
elif event_label == "P":
|
|
self.l10n_be_P_event_counter += 1
|
|
self.l10n_be_P_event_amount += self.currency_id.round(amount)
|
|
elif event_label == "I":
|
|
self.l10n_be_I_event_counter += 1
|
|
self.l10n_be_I_event_amount += self.currency_id.round(amount)
|
|
elif event_label == "T":
|
|
self.l10n_be_T_event_counter += 1
|
|
self.l10n_be_T_event_amount += self.currency_id.round(amount)
|
|
|
|
def handle_blackbox_signature(self, signatures_batch):
|
|
""" Set the first and last FDM reference for the session based on the given blackbox signature. """
|
|
self.ensure_one()
|
|
for data in signatures_batch:
|
|
blackbox_signature = data.get('signature')
|
|
amount = data.get('amount', 0)
|
|
is_self_session = data.get('isSelfSession')
|
|
data = data.get('data', {})
|
|
|
|
if blackbox_signature.get('fdmRef') is not None and blackbox_signature['fdmRef'].get('eventLabel') is not None:
|
|
if is_self_session and blackbox_signature['fdmRef']['eventLabel'] not in ['R', 'C']:
|
|
if not self.first_fdm_ref:
|
|
self.first_fdm_ref = blackbox_signature['fdmRef']
|
|
self.last_fdm_ref = blackbox_signature['fdmRef']
|
|
if blackbox_signature['eventOperation'] == 'REPORT_TURNOVER_Z':
|
|
self.l10n_be_turnover_z_report_fdm_ref = blackbox_signature['fdmRef']
|
|
self.config_id.l10n_be_report_counter += 1
|
|
elif blackbox_signature['eventOperation'] == 'REPORT_USER_Z':
|
|
self.l10n_be_user_z_report_fdm_ref = blackbox_signature['fdmRef']
|
|
self.config_id.l10n_be_report_counter += 1
|
|
elif blackbox_signature['eventOperation'] == 'DRAWER_OPEN':
|
|
self.l10n_be_sign_drawer_open_amount += 1
|
|
|
|
if blackbox_signature['fdmRef']['eventLabel'] in ['N', 'P', 'I', 'T']:
|
|
self.increment_l10n_be_counters(
|
|
blackbox_signature['fdmRef']['eventLabel'],
|
|
amount,
|
|
)
|
|
# If it's a signOrder, we check if there was a negQuantityReason "CORRECTION", or "PRICE_CHANGE"
|
|
if blackbox_signature['eventOperation'] == 'ORDER':
|
|
transaction_lines = data.get('transaction', {}).get('transactionLines', [])
|
|
has_correction = False
|
|
has_price_change = False
|
|
correction_qty = 0
|
|
correction_amount = 0.0
|
|
price_change_qty = 0
|
|
price_change_amount = 0.0
|
|
for line in transaction_lines:
|
|
main_product = line.get('mainProduct', {})
|
|
neg_reason = main_product.get('negQuantityReason')
|
|
quantity = main_product.get('quantity', 0)
|
|
line_total = line.get('lineTotal', 0.0)
|
|
if neg_reason == 'CORRECTION':
|
|
has_correction = True
|
|
correction_qty += abs(quantity)
|
|
correction_amount += line_total
|
|
elif neg_reason == 'PRICE_CHANGE':
|
|
has_price_change = True
|
|
price_change_qty += abs(quantity)
|
|
price_change_amount += line_total
|
|
|
|
if has_correction:
|
|
self.l10n_be_correction_qty_count += correction_qty
|
|
self.l10n_be_correction_ticket_count += 1
|
|
self.l10n_be_correction_amount += correction_amount
|
|
if has_price_change:
|
|
self.l10n_be_price_change_qty_count += price_change_qty
|
|
self.l10n_be_price_change_ticket_count += 1
|
|
self.l10n_be_price_change_amount += price_change_amount
|
|
|
|
def get_l10n_be_user_report_data(self, before_closing=False):
|
|
""" Return all data used in the user report """
|
|
self.ensure_one()
|
|
orders = self.env["pos.order"].search([
|
|
("session_id", "=", self.id),
|
|
('state', 'in', ['paid', 'done']),
|
|
('l10n_be_short_signature', '!=', False)
|
|
])
|
|
|
|
user_report_data = {
|
|
'posDevices': self._get_l10n_be_pos_devices(orders),
|
|
'fdmDevices': self._get_l10n_be_fdm_devices(orders),
|
|
'users': self._get_l10n_be_users_data(orders),
|
|
}
|
|
if self.state == "closed" or before_closing:
|
|
stop_at_dt = self.stop_at or fields.Datetime.now()
|
|
user_report_data["reportBookingDate"] = stop_at_dt.date().isoformat()
|
|
user_report_data["reportNo"] = self.config_id.l10n_be_report_counter
|
|
user_report_data["l10n_be_user_z_report_fdm_ref"] = self.l10n_be_user_z_report_fdm_ref
|
|
user_report_data["stop_at"] = stop_at_dt
|
|
return user_report_data
|
|
|
|
def generate_l10n_z_reports_attachments(self, sale_details_data, user_report_data):
|
|
""" Generates both the Sale Details Z report and the User Z report and attaches them to the session """
|
|
self.ensure_one()
|
|
report_env = self.with_context(skip_sale_details_compute=True)
|
|
|
|
sale_pdf, _ = report_env.env['ir.actions.report']._render_qweb_pdf(
|
|
'point_of_sale.sale_details_report',
|
|
data=sale_details_data,
|
|
)
|
|
sale_attachment_name = f'{self.name.replace(" ", "_")}_Sale_Details_Report_Z.pdf'
|
|
|
|
sale_attach = self.env['ir.attachment'].create({
|
|
'name': sale_attachment_name,
|
|
'type': 'binary',
|
|
'datas': base64.b64encode(sale_pdf),
|
|
'res_model': 'pos.session',
|
|
'res_id': self.id,
|
|
'mimetype': 'application/pdf',
|
|
})
|
|
|
|
self.message_post(
|
|
body="Sale Details Report Z has been generated.",
|
|
attachment_ids=[sale_attach.id],
|
|
)
|
|
|
|
user_pdf, _ = report_env.env['ir.actions.report']._render_qweb_pdf(
|
|
'l10n_be_pos_blackbox.user_report_template',
|
|
[self.id],
|
|
data=user_report_data,
|
|
)
|
|
user_attachment_name = f'{self.name.replace(" ", "_")}_Users_Report_Z.pdf'
|
|
|
|
user_attach = self.env['ir.attachment'].create({
|
|
'name': user_attachment_name,
|
|
'type': 'binary',
|
|
'datas': base64.b64encode(user_pdf),
|
|
'res_model': 'pos.session',
|
|
'res_id': self.id,
|
|
'mimetype': 'application/pdf',
|
|
})
|
|
|
|
self.message_post(
|
|
body="User Report Z has been generated.",
|
|
attachment_ids=[user_attach.id],
|
|
)
|
|
|
|
def _create_clock_events(self, cashiers, action):
|
|
self.ensure_one()
|
|
if not cashiers:
|
|
return
|
|
|
|
vals_list = []
|
|
for cashier in cashiers:
|
|
vals = {
|
|
'type': action,
|
|
'session_id': self.id,
|
|
}
|
|
if self.config_id.module_pos_hr:
|
|
vals['employee_id'] = cashier.id
|
|
else:
|
|
vals['user_id'] = cashier.id
|
|
|
|
vals_list.append(vals)
|
|
|
|
self.env['pos.clock.in.out'].create(vals_list)
|
|
|
|
def _get_l10n_be_cashier_social_events(self, cashier_clock_events):
|
|
""" Return the social events (clock in/out) of a cashier. """
|
|
social_events = []
|
|
for event in cashier_clock_events:
|
|
social_events.append({
|
|
'inOut': event.type.upper(),
|
|
'posDateTime': self.rfc3339_datetime(event.event_date_time),
|
|
})
|
|
return social_events
|
|
|
|
def _get_l10n_be_users_data(self, orders):
|
|
""" Return summarized users (cashiers) information from the given orders. """
|
|
users = []
|
|
# itertools.groupby() only groups consecutive elements with the same key.
|
|
orders = orders.sorted(lambda order: order._get_cashier().id)
|
|
for cashier, group in groupby(orders, lambda order: order._get_cashier()):
|
|
insz = cashier.sudo().l10n_be_insz_or_bis_number or cashier.sudo().employee_id.l10n_be_insz_or_bis_number
|
|
cashier_clock_events = cashier.pos_clock_in_out_ids.filtered(lambda e: e.session_id == self)
|
|
group_orders = list(group)
|
|
|
|
if cashier_clock_events:
|
|
first_pos_dt = min(cashier_clock_events.mapped('event_date_time'))
|
|
last_pos_dt = max(cashier_clock_events.mapped('event_date_time'))
|
|
else:
|
|
# FIXME: this is a temporary fallback for mobile orders without clock events
|
|
# TODO-manv: FW-19.1: directly contact OBOX via backend to sign mobile orders
|
|
# We will also first ensure that the cashier is clocked in (for self-ordering), so we should always have clock events for the cashier
|
|
first_pos_dt = min(order.date_order for order in group_orders)
|
|
last_pos_dt = max(order.date_order for order in group_orders)
|
|
|
|
cashier_data = {
|
|
'employeeId': insz,
|
|
'totalAmount': 0,
|
|
'socialEvents': self._get_l10n_be_cashier_social_events(cashier_clock_events),
|
|
'firstPosDateTime': self.rfc3339_datetime(first_pos_dt),
|
|
'lastPosDateTime': self.rfc3339_datetime(last_pos_dt),
|
|
# Leading '_' keeps this field for reports only; it's filtered out before sending to the blackbox in js side (see 'extractFdmData()').
|
|
'_name': cashier.name,
|
|
}
|
|
cashier_payments = {}
|
|
for order in group_orders:
|
|
cashier_data['totalAmount'] = self.currency_id.round(cashier_data['totalAmount'] + order.amount_paid)
|
|
for payment in order.payment_ids:
|
|
pm_id = str(payment.payment_method_id.id)
|
|
pm_entry = cashier_payments.get(pm_id)
|
|
if pm_entry:
|
|
cashier_payments[pm_id]['amount']['normalAmount'] = self.currency_id.round(cashier_payments[pm_id]['amount']['normalAmount'] + payment.amount)
|
|
cashier_payments[pm_id]['amount']['totalAmount'] = self.currency_id.round(cashier_payments[pm_id]['amount']['totalAmount'] + payment.amount)
|
|
else:
|
|
cashier_payments[pm_id] = {
|
|
'id': pm_id,
|
|
'name': (payment.payment_method_id.name or "").strip(),
|
|
'type': payment.payment_method_id._get_mapped_payment_type(),
|
|
'amount': {
|
|
'type': "PAYMENT",
|
|
'totalAmount': self.currency_id.round(payment.amount),
|
|
'normalAmount': self.currency_id.round(payment.amount),
|
|
'negativeCorrections': 0,
|
|
'positiveCorrections': 0,
|
|
'correctionsCount': 0,
|
|
}
|
|
}
|
|
cashier_data['payments'] = list(cashier_payments.values())
|
|
users.append(cashier_data)
|
|
return users
|
|
|
|
def _get_l10n_be_invoices(self, data_invoices):
|
|
"""Return summarized invoices information from the given data invoices."""
|
|
invoices = []
|
|
for invoice in data_invoices:
|
|
invoices.append({
|
|
"invoiceNo": f"invoice-{invoice.get('id')}",
|
|
"amount": invoice.get("total"),
|
|
})
|
|
return invoices
|
|
|
|
def _get_l10n_be_payments(self, data_payments):
|
|
"""Return summarized payments information from the given data payments."""
|
|
payments = []
|
|
for payment in data_payments:
|
|
pm = self.env['pos.payment.method'].browse(payment.get("id"))
|
|
payments.append({
|
|
"id": str(payment.get("id")),
|
|
"name": (payment.get("name") or "").strip(),
|
|
"type": pm._get_mapped_payment_type(),
|
|
"amount": {
|
|
"type": "PAYMENT",
|
|
"totalAmount": payment.get("total"),
|
|
"normalAmount": payment.get("total"),
|
|
"negativeCorrections": 0,
|
|
"positiveCorrections": 0,
|
|
"correctionsCount": 0,
|
|
}
|
|
})
|
|
return payments
|
|
|
|
def _get_l10n_be_vats(self, data_taxes, data_refund_taxes):
|
|
"""Return summarized VAT information from the given data taxes and refund taxes."""
|
|
vats = defaultdict(lambda: {
|
|
"taxableAmount": 0,
|
|
"vatAmount": 0,
|
|
"totalAmount": 0,
|
|
"rate": 0,
|
|
"label": ""
|
|
})
|
|
|
|
for tax in (*data_taxes, *data_refund_taxes):
|
|
label = tax.get("identification_letter")
|
|
base, tax_amount = tax.get("base_amount", 0), tax.get("tax_amount", 0)
|
|
total = base + tax_amount
|
|
vat = vats[label]
|
|
vat.update(label=label, rate=tax.get("rate"))
|
|
vat["taxableAmount"] += base
|
|
vat["vatAmount"] += tax_amount
|
|
vat["totalAmount"] += total
|
|
|
|
for vat in vats.values():
|
|
vat["taxableAmount"] = self.currency_id.round(vat["taxableAmount"])
|
|
vat["vatAmount"] = self.currency_id.round(vat["vatAmount"])
|
|
vat["totalAmount"] = self.currency_id.round(vat["totalAmount"])
|
|
return list(vats.values())
|
|
|
|
def _get_l10n_be_pos_devices(self, orders):
|
|
"""Return summarized POS devices information from the given orders.
|
|
|
|
Each entry groups orders by (posId, terminalId) and provides:
|
|
- posId: the POS ID (l10n_be_pos_id)
|
|
- terminalId: the terminal ID (l10n_be_terminal_id)
|
|
- firstPosDateTime: earliest l10n_be_pos_date_time
|
|
- lastPosDateTime: latest l10n_be_pos_date_time
|
|
"""
|
|
pos_devices_map = defaultdict(list)
|
|
|
|
for order in orders:
|
|
pos_id = order.l10n_be_pos_id
|
|
terminal_id = order.l10n_be_terminal_id
|
|
if not pos_id or not terminal_id:
|
|
continue
|
|
pos_devices_map[pos_id, terminal_id].append(order.l10n_be_pos_date_time)
|
|
|
|
pos_devices = []
|
|
for (pos_id, terminal_id), date_list in pos_devices_map.items():
|
|
first_dt = min(date_list)
|
|
last_dt = max(date_list)
|
|
|
|
pos_devices.append({
|
|
"posId": pos_id,
|
|
"terminalId": terminal_id,
|
|
"firstPosDateTime": self.rfc3339_datetime(first_dt),
|
|
"lastPosDateTime": self.rfc3339_datetime(last_dt),
|
|
})
|
|
|
|
return pos_devices
|
|
|
|
def _get_l10n_be_fdm_devices(self, orders):
|
|
"""Return summarized FDM device information from the given orders.
|
|
|
|
Each entry groups orders by FDM ID and provides:
|
|
- fdmId: the FDM ID (l10n_be_fdm_id)
|
|
- firstFdmDateTime: earliest l10n_be_fdm_date_time
|
|
- firstTotalCounter: total counter at that first FDM datetime
|
|
- lastFdmDateTime: latest l10n_be_fdm_date_time
|
|
- lastTotalCounter: total counter at that last FDM datetime
|
|
"""
|
|
fdm_map = defaultdict(list)
|
|
|
|
for order in orders:
|
|
fdm_id = order.l10n_be_fdm_id
|
|
if not fdm_id:
|
|
continue
|
|
fdm_map[fdm_id].append(order)
|
|
|
|
fdm_devices = []
|
|
for fdm_id, fdm_orders in fdm_map.items():
|
|
fdm_orders.sort(key=lambda o: o.l10n_be_fdm_date_time)
|
|
|
|
first_order = fdm_orders[0]
|
|
last_order = fdm_orders[-1]
|
|
|
|
fdm_devices.append({
|
|
"fdmId": fdm_id,
|
|
"firstFdmDateTime": self.rfc3339_datetime_utc(first_order.l10n_be_fdm_date_time),
|
|
"firstTotalCounter": first_order.l10n_be_total_counter,
|
|
"lastFdmDateTime": self.rfc3339_datetime_utc(last_order.l10n_be_fdm_date_time),
|
|
"lastTotalCounter": last_order.l10n_be_total_counter,
|
|
})
|
|
|
|
return fdm_devices
|
|
|
|
def _get_l10n_be_departments(self, product_data, refund_product_data):
|
|
"""Return summarized departments (category) information from the given product data. """
|
|
departments = []
|
|
|
|
def add_department_id(products):
|
|
for dept in products:
|
|
first_dep_product = self.env["product.product"].browse(dept['products'][0]['product_id'])
|
|
is_dep = first_dep_product and first_dep_product.pos_categ_ids
|
|
dep_id = first_dep_product.pos_categ_ids[0].id if is_dep else 0
|
|
dept['id'] = dep_id
|
|
add_department_id(product_data)
|
|
add_department_id(refund_product_data)
|
|
|
|
merged_products = defaultdict(dict)
|
|
# Start with product_data as positive
|
|
for row in product_data:
|
|
rid = row["id"]
|
|
if not merged_products[rid]:
|
|
merged_products[rid]["id"] = rid
|
|
merged_products[rid]["name"] = row.get("name")
|
|
merged_products[rid]["total"] = merged_products[rid].get("total", 0) + row.get("total", 0)
|
|
|
|
# Subtract refund_product_data
|
|
for row in refund_product_data:
|
|
rid = row["id"]
|
|
if not merged_products[rid]:
|
|
merged_products[rid]["id"] = rid
|
|
merged_products[rid]["name"] = row.get("name")
|
|
merged_products[rid]["total"] = merged_products[rid].get("total", 0) - row.get("total", 0)
|
|
|
|
merged_products = list(merged_products.values())
|
|
|
|
for dept in merged_products:
|
|
departments.append({
|
|
"departmentId": str(dept["id"]),
|
|
"departmentName": (dept.get("name") or "").strip(),
|
|
"amount": self.currency_id.round(dept.get("total")),
|
|
})
|
|
|
|
return departments
|
|
|
|
def _get_l10n_be_turnover_transactions(self):
|
|
"""Return turnover transactions summary for the session."""
|
|
return [
|
|
{
|
|
"eventLabel": "N",
|
|
"ticketCount": self.l10n_be_N_event_counter,
|
|
"amount": self.l10n_be_N_event_amount,
|
|
},
|
|
{
|
|
"eventLabel": "P",
|
|
"ticketCount": self.l10n_be_P_event_counter,
|
|
"amount": self.l10n_be_P_event_amount,
|
|
},
|
|
{
|
|
"eventLabel": "I",
|
|
"ticketCount": self.l10n_be_I_event_counter,
|
|
"amount": self.l10n_be_I_event_amount,
|
|
},
|
|
{
|
|
"eventLabel": "T",
|
|
"ticketCount": self.l10n_be_T_event_counter,
|
|
"amount": self.l10n_be_T_event_amount,
|
|
}
|
|
]
|
|
|
|
def _get_l10n_be_neg_quantities(self, orders):
|
|
""" Return negative quantities summary from the given orders. """
|
|
res = []
|
|
refund_orders = orders.filtered(lambda o: o.is_refund)
|
|
if refund_orders:
|
|
res.append({
|
|
"negQuantityReason": "REFUND",
|
|
"ticketCount": len(refund_orders),
|
|
"negQuantityCount": len(refund_orders.mapped('lines')),
|
|
"amount": sum(refund_orders.mapped('amount_paid')),
|
|
})
|
|
if self.l10n_be_correction_ticket_count:
|
|
res.append({
|
|
"negQuantityReason": "CORRECTION",
|
|
"ticketCount": self.l10n_be_correction_ticket_count,
|
|
"negQuantityCount": self.l10n_be_correction_qty_count,
|
|
"amount": self.currency_id.round(self.l10n_be_correction_amount),
|
|
})
|
|
if self.l10n_be_price_change_ticket_count:
|
|
res.append({
|
|
"negQuantityReason": "PRICE_CHANGE",
|
|
"ticketCount": self.l10n_be_price_change_ticket_count,
|
|
"negQuantityCount": self.l10n_be_price_change_qty_count,
|
|
"amount": self.currency_id.round(self.l10n_be_price_change_amount),
|
|
})
|
|
return res
|
|
|
|
def _get_l10n_be_price_changes(self, orders):
|
|
"""Return summarized PUBLIC price changes from given orders."""
|
|
price_changes = []
|
|
vats = orders.lines.mapped('l10n_be_vats')
|
|
|
|
totals = defaultdict(lambda: {'negative': 0.0, 'positive': 0.0})
|
|
|
|
for vat_list in vats:
|
|
if not vat_list:
|
|
continue
|
|
for vat in vat_list:
|
|
label = vat.get('label')
|
|
for change in vat.get('priceChanges', []):
|
|
if change.get('type') != 'PUBLIC':
|
|
continue
|
|
key = (change['id'], change['name'], change['type'], label)
|
|
amount = change.get('amount', 0.0)
|
|
if amount < 0:
|
|
totals[key]['negative'] += amount
|
|
elif amount > 0:
|
|
totals[key]['positive'] += amount
|
|
|
|
grouped = defaultdict(list)
|
|
for (cid, name, ctype, label), vals in totals.items():
|
|
grouped[cid, name, ctype].append({
|
|
'label': label,
|
|
'negative': round(vals['negative'], 2),
|
|
'positive': round(vals['positive'], 2),
|
|
})
|
|
|
|
for (cid, name, ctype), amounts in grouped.items():
|
|
price_changes.append({
|
|
'id': str(cid),
|
|
'name': name,
|
|
'type': ctype,
|
|
'amount': amounts,
|
|
})
|
|
|
|
return price_changes
|
|
|
|
def rfc3339_datetime(self, dt):
|
|
"""
|
|
Convert a datetime object to an RFC 3339 formatted string.
|
|
This is the required format to communicate POS date time
|
|
Example:
|
|
>>> rfc3339_datetime(datetime(2024, 6, 1, 12, 0, 0))
|
|
'2024-06-01T12:00:00.000+02:00'
|
|
"""
|
|
brussels = pytz.timezone("Europe/Brussels")
|
|
local_dt = brussels.localize(dt) if dt.tzinfo is None else dt.astimezone(brussels)
|
|
return local_dt.isoformat(timespec='milliseconds')
|
|
|
|
def rfc3339_datetime_utc(self, dt):
|
|
"""
|
|
Convert a datetime to an RFC 3339 UTC datetime formatted string.
|
|
This is the required format to communicate FDM date time
|
|
Example:
|
|
>>> rfc3339_datetime_utc(datetime(2024, 6, 1, 12, 0, 0))
|
|
'2024-06-01T10:00:00.000Z'
|
|
"""
|
|
brussels = pytz.timezone("Europe/Brussels")
|
|
local_dt = brussels.localize(dt) if dt.tzinfo is None else dt.astimezone(brussels)
|
|
utc_dt = local_dt.astimezone(pytz.UTC)
|
|
return utc_dt.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
|