From 9df63f790bd9d31d72930bc4197172a7bbc7d977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 17 Apr 2024 11:27:24 +0200 Subject: [PATCH] [IMP]pms_api_rest: Imrpovement API logs, PUT folio service and action_confirm reservation --- pms_api_rest/__manifest__.py | 1 + pms_api_rest/data/cron_jobs.xml | 20 ++ pms_api_rest/models/pms_api_log.py | 74 ++-- pms_api_rest/models/pms_folio.py | 13 +- pms_api_rest/models/pms_property.py | 176 ++++++---- pms_api_rest/services/pms_folio_service.py | 318 +++++++++++------- .../services/pms_reservation_service.py | 205 +++++------ 7 files changed, 486 insertions(+), 321 deletions(-) create mode 100644 pms_api_rest/data/cron_jobs.xml diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 80661e978..73f298c56 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -25,6 +25,7 @@ "data/sql_reports.xml", "data/auth_jwt_validator.xml", "data/pms_app_reset_password_template.xml", + "data/cron_jobs.xml", "views/pms_property_views.xml", "views/res_users_views.xml", "views/pms_room_type_class_views.xml", diff --git a/pms_api_rest/data/cron_jobs.xml b/pms_api_rest/data/cron_jobs.xml new file mode 100644 index 000000000..650b36789 --- /dev/null +++ b/pms_api_rest/data/cron_jobs.xml @@ -0,0 +1,20 @@ + + + + + Clean Log PMS API REST + 1 + + days + -1 + + code + + + model.clean_log_data(offset=60) + + + diff --git a/pms_api_rest/models/pms_api_log.py b/pms_api_rest/models/pms_api_log.py index 9e3dcb2bf..0b0a9f7ae 100644 --- a/pms_api_rest/models/pms_api_log.py +++ b/pms_api_rest/models/pms_api_log.py @@ -1,4 +1,6 @@ -from odoo import _, api, fields, models +from datetime import timedelta + +from odoo import _, fields, models class PmsApiLog(models.Model): @@ -72,10 +74,39 @@ class PmsApiLog(models.Model): string="Response URL", help="Response URL", ) - model_id = fields.Many2one( - string="Model", - help="Model", - comodel_name="ir.model", + request_type = fields.Selection( + string="Request Type", + help="Request Type", + selection=[ + ("folios", "Folios"), + ("availability", "Availability"), + ("restrictions", "Restrictions rules"), + ("prices", "Prices"), + ], + ) + target_date_from = fields.Date( + string="Target Date From", + help="Target Date From", + ) + target_date_to = fields.Date( + string="Target Date To", + help="Target Date To", + ) + folio_ids = fields.Many2many( + string="Folios", + help="Folios", + comodel_name="pms.folio", + relation="pms_folio_pms_api_log_rel", + column1="pms_api_log_ids", + column2="folio_ids", + ) + room_type_ids = fields.Many2many( + string="Room Types", + help="Room Types", + comodel_name="pms.room.type", + relation="pms_room_type_pms_api_log_rel", + column1="pms_api_log_ids", + column2="room_type_ids", ) def related_action_open_record(self): @@ -90,10 +121,7 @@ class PmsApiLog(models.Model): """ self.ensure_one() - if "pms_api_log_id" in self.env[self.model_id.model]._fields: - records = self.env[self.model_id.model].search( - [("pms_api_log_id", "=", self.id)] - ) + records = self.folio_ids if not records: return None action = { @@ -114,21 +142,15 @@ class PmsApiLog(models.Model): ) return action - @api.model - def create(self, vals): + def clean_log_data(self, offset=60): + """Clean log data older than the offset. + + :param int offset: The number of days to keep the log data. + """ - set pms_api_log_id and origin_json in related records - if record_ids id present in context - """ - log_record = super().create(vals) - if self.env.context.get("record_ids"): - records = self.env[self.env.context.get("model")].browse( - self.env.context.get("record_ids") - ) - records.write( - { - "pms_api_log_id": log_record.id, - "origin_json": log_record.request, - } - ) - return log_record + self.sudo().search( + [ + ("status", "=", "success"), + ("create_date", "<", fields.Datetime.now() - timedelta(days=offset)), + ] + ).unlink() diff --git a/pms_api_rest/models/pms_folio.py b/pms_api_rest/models/pms_folio.py index 182f231a9..b0ff25c59 100644 --- a/pms_api_rest/models/pms_folio.py +++ b/pms_api_rest/models/pms_folio.py @@ -4,12 +4,11 @@ from odoo import fields, models class PmsFolio(models.Model): _inherit = "pms.folio" - pms_api_log_id = fields.Many2one( - string="PMS API Log", - help="PMS API Log", + pms_api_log_ids = fields.Many2many( + string="API Logs", + help="API Logs", comodel_name="pms.api.log", - ) - origin_json = fields.Text( - string="Origin JSON", - help="Origin JSON", + relation="pms_folio_pms_api_log_rel", + column1="folio_ids", + column2="pms_api_log_ids", ) diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index 45d558330..384249d11 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -573,6 +573,9 @@ class PmsProperty(models.Model): clients = client else: clients = self.env["res.users"].search([("pms_api_client", "=", True)]) + room_type_ids = [] + endpoint = "" + response = None _logger.info("PMS API push batch") if isinstance(date_from, str): date_from = datetime.datetime.strptime(date_from, "%Y-%m-%d").date() @@ -593,73 +596,112 @@ class PmsProperty(models.Model): ] ) for pms_property in pms_properties: - property_client_conf = self.env["ota.property.settings"].search( - [ - ("pms_property_id", "=", pms_property.id), - ("agency_id", "=", client.partner_id.id), - ] - ) - pms_property_id = pms_property.id - room_type_ids = ( - [filter_room_type_id] - if filter_room_type_id - else self.env["pms.room"] - .search([("pms_property_id", "=", pms_property_id)]) - .mapped("room_type_id") - .filtered( - lambda r: r.id - not in property_client_conf.excluded_room_type_ids.ids + try: + property_client_conf = ( + self.env["ota.property.settings"] + .sudo() + .search( + [ + ("pms_property_id", "=", pms_property.id), + ("agency_id", "=", client.partner_id.id), + ] + ) ) - .ids - ) - payload = { - "pmsPropertyId": pms_property_id, - } - data = [] - for room_type_id in room_type_ids: - if call_type == "availability": - endpoint = client.url_endpoint_availability - data.extend( - pms_property.generate_availability_json( - date_from=date_from, - date_to=date_to, - pms_property_id=pms_property_id, - room_type_id=room_type_id, - client=client, - ) + pms_property_id = pms_property.id + room_type_ids = ( + [filter_room_type_id] + if filter_room_type_id + else self.env["pms.room"] + .search([("pms_property_id", "=", pms_property_id)]) + .mapped("room_type_id") + .filtered( + lambda r: r.id + not in property_client_conf.excluded_room_type_ids.ids ) - key_data = "avails" - elif call_type == "restrictions": - endpoint = client.url_endpoint_rules - data.extend( - pms_property.generate_restrictions_json( - date_from=date_from, - date_to=date_to, - pms_property_id=pms_property_id, - room_type_id=room_type_id, - client=client, - ) - ) - key_data = "rules" - elif call_type == "prices": - endpoint = client.url_endpoint_prices - data.extend( - pms_property.generate_prices_json( - date_from=date_from, - date_to=date_to, - pms_property_id=pms_property_id, - room_type_id=room_type_id, - client=client, - ) - ) - key_data = "prices" - else: - raise ValidationError(_("Invalid call type")) - if data: - payload[key_data] = data - response = self.pms_api_push_payload(payload, endpoint, client) - _logger.info( - f"""PMS API push batch response to - {endpoint}: {response.status_code} - {response.text}""" + .ids + ) + payload = { + "pmsPropertyId": pms_property_id, + } + data = [] + for room_type_id in room_type_ids: + if call_type == "availability": + endpoint = client.url_endpoint_availability + data.extend( + pms_property.generate_availability_json( + date_from=date_from, + date_to=date_to, + pms_property_id=pms_property_id, + room_type_id=room_type_id, + client=client, + ) + ) + key_data = "avails" + elif call_type == "restrictions": + endpoint = client.url_endpoint_rules + data.extend( + pms_property.generate_restrictions_json( + date_from=date_from, + date_to=date_to, + pms_property_id=pms_property_id, + room_type_id=room_type_id, + client=client, + ) + ) + key_data = "rules" + elif call_type == "prices": + endpoint = client.url_endpoint_prices + data.extend( + pms_property.generate_prices_json( + date_from=date_from, + date_to=date_to, + pms_property_id=pms_property_id, + room_type_id=room_type_id, + client=client, + ) + ) + key_data = "prices" + else: + raise ValidationError(_("Invalid call type")) + if data: + payload[key_data] = data + response = self.pms_api_push_payload(payload, endpoint, client) + _logger.info( + f"""PMS API push batch response to + {endpoint}: {response.status_code} - {response.text}""" + ) + self.invalidate_cache() + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": pms_property_id, + "client_id": client.id, + "request": payload, + "response": str(response), + "status": "success" if response.ok else "error", + "request_date": fields.Datetime.now(), + "method": "PUSH", + "endpoint": endpoint, + "target_date_from": date_from, + "target_date_to": date_to, + "request_type": call_type, + "room_type_ids": room_type_ids, + } + ) + except Exception as e: + _logger.error(f"""PMS API push batch error: {e}""") + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": pms_property_id, + "client_id": client.id, + "request": payload, + "response": str(e), + "status": "error", + "request_date": fields.Datetime.now(), + "method": "PUSH", + "endpoint": endpoint, + "target_date_from": date_from, + "target_date_to": date_to, + "request_type": call_type, + "room_type_ids": room_type_ids, + } ) - self.invalidate_cache() diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 059066bfc..d0269de9e 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1,4 +1,3 @@ -import ast import base64 import logging from datetime import datetime, timedelta @@ -584,6 +583,12 @@ class PmsFolioService(Component): def create_folio(self, pms_folio_info): external_app = self.env.user.pms_api_client log_payload = pms_folio_info + min_checkin_payload = min( + pms_folio_info.reservations, key=lambda x: x.checkin + ).checkin + max_checkout_payload = max( + pms_folio_info.reservations, key=lambda x: x.checkout + ).checkout try: if pms_folio_info.reservationType == "out": vals = { @@ -760,7 +765,7 @@ class PmsFolioService(Component): force_write_blocked=True if external_app else False, )._compute_board_service_room_id() pms_folio_info.transactions = self.normalize_payments_structure( - pms_folio_info + pms_folio_info, folio ) if pms_folio_info.transactions: self.compute_transactions(folio, pms_folio_info.transactions) @@ -793,9 +798,7 @@ class PmsFolioService(Component): date_to=date_to, ) if external_app: - self.env["pms.api.log"].with_context( - record_ids=folio.ids - ).sudo().create( + self.env["pms.api.log"].sudo().create( { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, @@ -805,9 +808,10 @@ class PmsFolioService(Component): "request_date": fields.Datetime.now(), "method": "POST", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": folio.ids, + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) return folio.id @@ -827,9 +831,10 @@ class PmsFolioService(Component): "request_date": fields.Datetime.now(), "method": "POST", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": [], + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) if not external_app: @@ -844,52 +849,62 @@ class PmsFolioService(Component): reference += transaction.reference else: raise ValidationError(_("The transaction reference is required")) - if not self.env["account.payment"].search( + proposed_transaction = self.env["account.payment"].search( [ ("pms_property_id", "=", folio.pms_property_id.id), ("payment_type", "=", transaction.transactionType), ("folio_ids", "in", folio.id), - ("ref", "ilike", transaction.reference), + ("ref", "ilike", reference), + ("state", "=", "posted"), ] + ) + if ( + not proposed_transaction + or proposed_transaction.amount != transaction.amount ): - journal = self.env["account.journal"].search( - [("id", "=", transaction.journalId)] - ) - if not journal: - ota_conf = self.env["ota.property.settings"].search( - [ - ("pms_property_id", "=", folio.pms_property_id.id), - ("agency_id", "=", self.env.user.partner_id.id), - ] - ) - if ota_conf: - journal = ota_conf.pms_api_payment_journal_id - if transaction.transactionType == "inbound": - folio.do_payment( - journal, - journal.suspense_account_id, - self.env.user, - transaction.amount, - folio, - reservations=False, - services=False, - partner=False, - date=datetime.strptime(transaction.date, "%Y-%m-%d"), - ref=reference, - ) - elif transaction.transactionType == "outbound": - folio.do_refund( - journal, - journal.suspense_account_id, - self.env.user, - transaction.amount, - folio, - reservations=False, - services=False, - partner=False, - date=datetime.strptime(transaction.date, "%Y-%m-%d"), - ref=reference, + if proposed_transaction: + proposed_transaction.action_draft() + proposed_transaction.amount = transaction.amount + proposed_transaction.action_post() + else: + journal = self.env["account.journal"].search( + [("id", "=", transaction.journalId)] ) + if not journal: + ota_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", folio.pms_property_id.id), + ("agency_id", "=", self.env.user.partner_id.id), + ] + ) + if ota_conf: + journal = ota_conf.pms_api_payment_journal_id + if transaction.transactionType == "inbound": + folio.do_payment( + journal, + journal.suspense_account_id, + self.env.user, + transaction.amount, + folio, + reservations=False, + services=False, + partner=False, + date=datetime.strptime(transaction.date, "%Y-%m-%d"), + ref=reference, + ) + elif transaction.transactionType == "outbound": + folio.do_refund( + journal, + journal.suspense_account_id, + self.env.user, + transaction.amount, + folio, + reservations=False, + services=False, + partner=False, + date=datetime.strptime(transaction.date, "%Y-%m-%d"), + ref=reference, + ) @restapi.method( [ @@ -913,7 +928,7 @@ class PmsFolioService(Component): folio.action_cancel() if pms_folio_info.confirmReservations: for reservation in folio.reservation_ids: - reservation.confirm() + reservation.action_confirm() if pms_folio_info.internalComment is not None: folio_vals.update({"internal_comment": pms_folio_info.internalComment}) if pms_folio_info.partnerId: @@ -1593,6 +1608,12 @@ class PmsFolioService(Component): def update_put_external_folio(self, external_reference, pms_folio_info): external_app = self.env.user.pms_api_client log_payload = pms_folio_info + min_checkin_payload = min( + pms_folio_info.reservations, key=lambda x: x.checkin + ).checkin + max_checkout_payload = max( + pms_folio_info.reservations, key=lambda x: x.checkout + ).checkout try: folio = self.env["pms.folio"].search( [ @@ -1603,7 +1624,7 @@ class PmsFolioService(Component): if not folio or len(folio) > 1: raise MissingError(_("Folio not found")) self.update_folio_values(folio, pms_folio_info) - self.env["pms.api.log"].with_context(record_ids=folio.ids).sudo().create( + self.env["pms.api.log"].sudo().create( { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, @@ -1613,9 +1634,10 @@ class PmsFolioService(Component): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": folio.ids, + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) return folio.id @@ -1635,9 +1657,10 @@ class PmsFolioService(Component): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": [], + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) if not external_app: @@ -1660,12 +1683,18 @@ class PmsFolioService(Component): def update_put_folio(self, folio_id, pms_folio_info): external_app = self.env.user.pms_api_client log_payload = pms_folio_info + min_checkin_payload = min( + pms_folio_info.reservations, key=lambda x: x.checkin + ).checkin + max_checkout_payload = max( + pms_folio_info.reservations, key=lambda x: x.checkout + ).checkout try: folio = self.env["pms.folio"].browse(folio_id) if not folio: raise MissingError(_("Folio not found")) self.update_folio_values(folio, pms_folio_info) - self.env["pms.api.log"].with_context(record_ids=folio.ids).sudo().create( + self.env["pms.api.log"].sudo().create( { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, @@ -1675,9 +1704,10 @@ class PmsFolioService(Component): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": folio.ids, + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) @@ -1698,9 +1728,10 @@ class PmsFolioService(Component): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": [], + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) if not external_app: @@ -1710,17 +1741,11 @@ class PmsFolioService(Component): def update_folio_values(self, folio, pms_folio_info): external_app = self.env.user.pms_api_client - origin_values_dict = False - if external_app: - origin_values_dict = ast.literal_eval(folio.origin_json) - if origin_values_dict: - # Compare the values of the origin folio with the new values - # and set the new value to None if it is the same as the origin value - for key, value in origin_values_dict: - if value == pms_folio_info[key]: - pms_folio_info[key] = None folio_vals = {} - if pms_folio_info.state == "cancel": + if pms_folio_info.state == "cancel" and folio.state != "cancel": + draft_invoices = folio.invoice_ids.filtered(lambda i: i.state == "draft") + if draft_invoices: + draft_invoices.action_cancel() folio.action_cancel() return folio.id # if ( @@ -1731,7 +1756,7 @@ class PmsFolioService(Component): # ) # ): # for reservation in folio.reservation_ids: - # reservation.confirm() + # reservation.action_confirm() if ( pms_folio_info.internalComment is not None and pms_folio_info.internalComment not in folio.internal_comment @@ -1765,32 +1790,49 @@ class PmsFolioService(Component): folio_vals.update({"mobile": pms_folio_info.partnerPhone}) if ( self.get_language(pms_folio_info.language) - and self.get_language(pms_folio_info.language) != pms_folio_info.language + and self.get_language(pms_folio_info.language) != folio.lang ): folio_vals.update({"lang": self.get_language(pms_folio_info.language)}) + reservations_vals = [] if pms_folio_info.reservations: reservations_vals = self.wrapper_reservations( folio, pms_folio_info.reservations ) if reservations_vals: - folio_vals.update({"reservation_ids": reservations_vals}) - if folio_vals: - if reservations_vals: - # Cancel the old reservations that have not been included in the update update_reservation_ids = [] for val in reservations_vals: + # Cancel the old reservations that have not been included in the update if val[0] == 1: + if val[2].get("state") == "cancel": + self.env["pms.reservation"].with_context( + force_write_blocked=True + ).browse(val[1]).action_cancel() + # delete from reservations_vals the reservation that has been canceled + reservations_vals.pop(reservations_vals.index(val)) + if val[2].get("state") == "confirm": + self.env["pms.reservation"].with_context( + force_write_blocked=True + ).browse(val[1]).action_confirm() + # delete from reservations_vals the field state + val[2].pop("state") update_reservation_ids.append(val[1]) - folio.reservation_ids.filtered( + old_reservations_to_cancel = folio.reservation_ids.filtered( lambda r: r.state != "cancel" and r.id not in update_reservation_ids - ).with_context(modified=True, force_write_blocked=True).action_cancel() + ) + old_reservations_to_cancel.with_context( + modified=True, force_write_blocked=True + ).action_cancel() + folio_vals.update({"reservation_ids": reservations_vals}) + if folio_vals: folio.with_context( skip_compute_service_ids=False if external_app else True, force_overbooking=True if external_app else False, force_write_blocked=True if external_app else False, ).write(folio_vals) # Compute OTA transactions - pms_folio_info.transactions = self.normalize_payments_structure(pms_folio_info) + pms_folio_info.transactions = self.normalize_payments_structure( + pms_folio_info, folio + ) if pms_folio_info.transactions: self.compute_transactions(folio, pms_folio_info.transactions) # Force update availability @@ -1804,7 +1846,7 @@ class PmsFolioService(Component): date_to=date_to, ) - def normalize_payments_structure(self, pms_folio_info): + def normalize_payments_structure(self, pms_folio_info, folio): """ This method use the OTA payment structure to normalize the structure and incorporate them in the transactions datamodel param @@ -1821,6 +1863,17 @@ class PmsFolioService(Component): ("agency_id", "=", self.env.user.partner_id.id), ] ) + if not ota_conf: + raise ValidationError( + _("No OTA configuration found for this property") + ) + if not ota_conf.pms_api_payment_journal_id: + raise ValidationError( + _( + "No payment journal configured for this property for %s" + % ota_conf.name + ) + ) transaction.journalId = ota_conf.pms_api_payment_journal_id.id elif pms_folio_info.agencyId: ota_conf = self.env["ota.property.settings"].search( @@ -1849,7 +1902,7 @@ class PmsFolioService(Component): pmsTransactionInfo( journalId=journal.id, transactionType="inbound", - amount=pms_folio_info.totalPrice, + amount=round(folio.amount_total, 2), date=fields.Date.today().strftime("%Y-%m-%d"), reference=pms_folio_info.externalReference, ) @@ -1871,22 +1924,17 @@ class PmsFolioService(Component): # Search a reservation in saved_reservations whose sum of night amounts is equal # to the sum of night amounts of info_reservation, and dates equal, # if we find it we update it - payload_nights = round( - sum(info_reservation.reservationLines.mapped("price")), 2 - ) proposed_reservation = saved_reservations.filtered( - lambda r: r.checkin == info_reservation.checkin - and r.checkout == info_reservation.checkout - and r.room_type_id == info_reservation.roomTypeId - ).filtered( - lambda r: round( - sum(r.reservation_line_ids.mapped("price")) - + r.service_ids.filtered(lambda s: s.is_board_service).mapped(""), - 2, - ) - == payload_nights + lambda r: r.checkin + == datetime.strptime(info_reservation.checkin, "%Y-%m-%d").date() + and r.checkout + == datetime.strptime(info_reservation.checkout, "%Y-%m-%d").date() + and r.room_type_id.id == info_reservation.roomTypeId + and r.adults == info_reservation.adults + and r.children == info_reservation.children ) if proposed_reservation: + proposed_reservation = proposed_reservation[0] saved_reservations -= proposed_reservation vals = {} new_res = not proposed_reservation @@ -1900,12 +1948,17 @@ class PmsFolioService(Component): ): vals.update({"room_type_id": info_reservation.roomTypeId}) if info_reservation.checkin: - if new_res or proposed_reservation.checkin != info_reservation.checkin: + if ( + new_res + or proposed_reservation.checkin + != datetime.strptime(info_reservation.checkin, "%Y-%m-%d").date() + ): vals.update({"checkin": info_reservation.checkin}) if info_reservation.checkout: if ( new_res - or proposed_reservation.checkout != info_reservation.checkout + or proposed_reservation.checkout + != datetime.strptime(info_reservation.checkout, "%Y-%m-%d").date() ): vals.update({"checkout": info_reservation.checkout}) if info_reservation.pricelistId: @@ -1929,7 +1982,7 @@ class PmsFolioService(Component): if info_reservation.preferredRoomId: if ( new_res - or proposed_reservation.preferredRoomId + or proposed_reservation.preferred_room_id.id != info_reservation.preferredRoomId ): vals.update({"preferred_room_id": info_reservation.preferredRoomId}) @@ -1942,7 +1995,9 @@ class PmsFolioService(Component): or proposed_reservation.children != info_reservation.children ): vals.update({"children": info_reservation.children}) - if info_reservation.reservationLines and new_res: + if new_res or info_reservation.stateCode != proposed_reservation.state: + vals.update({"state": info_reservation.stateCode}) + if info_reservation.reservationLines: # The service price is included in day price when it is a board service (external api) board_day_price = 0 if external_app and vals.get("board_service_room_id"): @@ -1974,6 +2029,7 @@ class PmsFolioService(Component): reservation_lines_cmds = self.wrapper_reservation_lines( reservation=info_reservation, board_day_price=board_day_price, + proposed_reservation=proposed_reservation, ) if reservation_lines_cmds: vals.update({"reservation_line_ids": reservation_lines_cmds}) @@ -1994,20 +2050,38 @@ class PmsFolioService(Component): cmds.append((1, proposed_reservation.id, vals)) return cmds - def wrapper_reservation_lines(self, reservation, board_day_price=0): + def wrapper_reservation_lines( + self, reservation, board_day_price=0, proposed_reservation=False + ): cmds = [] for line in reservation.reservationLines: - cmds.append( - ( - 0, - False, - { - "date": line.date, - "price": line.price - board_day_price, - "discount": line.discount or 0, - }, + if proposed_reservation: + # Not is necesay check new dates, becouse a if the dates change, the reservation is new + proposed_line = proposed_reservation.reservation_line_ids.filtered( + lambda l: l.date == datetime.strptime(line.date, "%Y-%m-%d").date() ) - ) + if proposed_line: + vals = {} + if round(proposed_line.price, 2) != round( + line.price - board_day_price, 2 + ): + vals.update({"price": line.price - board_day_price}) + if round(proposed_line.discount, 2) != round(line.discount, 2): + vals.update({"discount": line.discount}) + if vals: + cmds.append((1, proposed_line.id, vals)) + else: + cmds.append( + ( + 0, + False, + { + "date": line.date, + "price": line.price - board_day_price, + "discount": line.discount or 0, + }, + ) + ) return cmds def wrapper_reservation_services(self, info_services, services=False): @@ -2045,17 +2119,21 @@ class PmsFolioService(Component): It is used to override potential availability changes on the channel made unilaterally, for example, upon entering or canceling a reservation. """ - api_clients = self.env["res.users"].search( - [ - ("pms_api_client", "=", True), - ("pms_property_ids", "in", pms_property_id), - ] + api_clients = ( + self.env["res.users"] + .sudo() + .search( + [ + ("pms_api_client", "=", True), + ("pms_property_ids", "in", pms_property_id), + ] + ) ) if not room_type_ids or not api_clients: return False for room_type_id in room_type_ids: pms_property = self.env["pms.property"].browse(pms_property_id) - self.env["pms.property"].pms_api_push_batch( + self.env["pms.property"].sudo().pms_api_push_batch( call_type="availability", # 'availability', 'prices', 'restrictions' date_from=date_from.strftime("%Y-%m-%d"), # 'YYYY-MM-DD' date_to=date_to.strftime("%Y-%m-%d"), # 'YYYY-MM-DD' diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 1ad3a0cd1..f6c771270 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -3,12 +3,12 @@ from datetime import datetime, timedelta from odoo import _, fields from odoo.exceptions import MissingError +from odoo.osv import expression +from odoo.tools.safe_eval import safe_eval from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component -from odoo.osv import expression -from odoo.tools.safe_eval import safe_eval class PmsReservationService(Component): @@ -284,13 +284,12 @@ class PmsReservationService(Component): if reservation_data.stateCode == "cancel": reservation.action_cancel() if reservation_data.stateCode == "confirm": - reservation.confirm() + reservation.action_confirm() if reservation_data.toCheckout is not None and reservation_data.toCheckout: reservation.action_reservation_checkout() if reservation_data.undoOnboard: reservation.action_undo_onboard() - def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): # Return dict witch reservation.lines values (only modified if line exist, # or all pass values if line not exist) @@ -587,7 +586,8 @@ class PmsReservationService(Component): if checkin_partner.document_number else None, documentExpeditionDate=datetime.combine( - checkin_partner.document_expedition_date, datetime.min.time() + checkin_partner.document_expedition_date, + datetime.min.time(), ).isoformat() if checkin_partner.document_expedition_date else None, @@ -658,7 +658,7 @@ class PmsReservationService(Component): checkin_partner.write( self.mapping_checkin_partner_values( pms_checkin_partner_info, - checkin_partner.partner_id.id if checkin_partner.partner_id else False + checkin_partner.partner_id.id if checkin_partner.partner_id else False, ) ) # if not partner_id we need to force compute to create partner @@ -860,7 +860,9 @@ class PmsReservationService(Component): checkin_partner.write( self.mapping_checkin_partner_values( pms_checkin_partner_info, - checkin_partner.partner_id.id if checkin_partner.partner_id else False + checkin_partner.partner_id.id + if checkin_partner.partner_id + else False, ) ) # if not partner_id we need to force compute to create partner @@ -884,7 +886,9 @@ class PmsReservationService(Component): if checkin_partner: checkin_partner.unlink() - def mapping_checkin_partner_values(self, pms_checkin_partner_info, partner_id=False): + def mapping_checkin_partner_values( + self, pms_checkin_partner_info, partner_id=False + ): vals = { "firstname": pms_checkin_partner_info.firstname, "lastname": pms_checkin_partner_info.lastname, @@ -1099,17 +1103,17 @@ class PmsReservationService(Component): ) def wizard_states(self, reservation_id): reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) - today = datetime.now().strftime('%Y-%m-%d') + today = datetime.now().strftime("%Y-%m-%d") wizard_states = [ { "code": "overbooking_with_availability", "title": "Overbooking", "domain": "[" - "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " - "('overbooking', '=', True), " - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " + "('overbooking', '=', True), " + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: r.count_alternative_free_rooms", "text": f"Parece que ha entrado una reserva sin haber disponibilidad para {reservation.room_type_id.name}.", "priority": 100, @@ -1118,64 +1122,64 @@ class PmsReservationService(Component): "code": "overbooking_without_availability", "title": "Overbooking", "domain": "[" - "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " - "('overbooking', '=', True), " - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " + "('overbooking', '=', True), " + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: r.count_alternative_free_rooms <= 0", "text": f"Parece que ha entrado una reserva sin haber disponibilidad para {reservation.room_type_id.name}." - f"Por desgracia no parece que hay ninguna " - f"habitación disponible con la capacidad suficiente para esta reserva", + f"Por desgracia no parece que hay ninguna " + f"habitación disponible con la capacidad suficiente para esta reserva", "priority": 150, }, { "code": "splitted_without_availability", "title": "Divididas", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('splitted', '=', True)," - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + "('splitted', '=', True)," + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: r.count_alternative_free_rooms <= 0", "text": f"Parece que a {reservation.partner_name} le ha tocado dormir en habitaciones diferentes " - f" pero no hay ninguna habitación disponible para asignarle, puedes probar a mover otras reservas " - f" para poder establecerle una única habitación. ", + f" pero no hay ninguna habitación disponible para asignarle, puedes probar a mover otras reservas " + f" para poder establecerle una única habitación. ", "priority": 200, }, { "code": "splitted_with_availability", "title": "Divididas", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('splitted', '=', True)," - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + "('splitted', '=', True)," + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: r.count_alternative_free_rooms", "text": f"Parece que a {reservation.partner_name} le ha tocado dormir en habitaciones diferentes" - f" pero tienes la posibilidad de moverlo a {reservation.count_alternative_free_rooms} " - f" {' habitación' if reservation.count_alternative_free_rooms == 1 else ' habitaciones'}.", + f" pero tienes la posibilidad de moverlo a {reservation.count_alternative_free_rooms} " + f" {' habitación' if reservation.count_alternative_free_rooms == 1 else ' habitaciones'}.", "priority": 220, }, { "code": "to_assign", "title": "Por asignar", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('to_assign', '=', True)," - "('reservation_type', 'in', ['normal', 'staff'])," - f"('checkin', '>=', '{today}')," - "]", + "('to_assign', '=', True)," + "('reservation_type', 'in', ['normal', 'staff'])," + f"('checkin', '>=', '{today}')," + "]", "text": f"La reserva de {reservation.partner_name} ha sido asignada a la habitación {reservation.preferred_room_id.name}," - " puedes confirmar la habitación o cambiar a otra desde aquí.", + " puedes confirmar la habitación o cambiar a otra desde aquí.", "priority": 300, }, { "code": "to_confirm", "title": "Por confirmar", "domain": "[('state', '=', 'draft')," - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])," - "]", + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])," + "]", "text": f"La reserva de {reservation.partner_name} está pendiente de confirmar, puedes confirmarla desde aquí.", "priority": 400, }, @@ -1183,36 +1187,36 @@ class PmsReservationService(Component): "code": "checkin_done_precheckin", "title": "Entrada Hoy", "domain": "[('state', '=', 'confirm')," - f"('checkin', '=', '{today}')," - "('pending_checkin_data', '=', 0)," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + f"('checkin', '=', '{today}')," + "('pending_checkin_data', '=', 0)," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "text": "Todos los huéspedes de esta reserva tienen los datos registrados, " - " puedes marcar la entrada directamente desde aquí", + " puedes marcar la entrada directamente desde aquí", "priority": 500, }, { "code": "checkin_partial_precheckin", "title": "Entrada Hoy", "domain": "[('state', '=', 'confirm')," - f"('checkin', '=', '{today}')," - "('pending_checkin_data', '>', 0)," - "('checkin_partner_ids.state','=', 'precheckin')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + f"('checkin', '=', '{today}')," + "('pending_checkin_data', '>', 0)," + "('checkin_partner_ids.state','=', 'precheckin')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "text": f"Faltan {reservation.pending_checkin_data} {' huésped ' if reservation.pending_checkin_data == 1 else ' huéspedes '} " - f"por registrar sus datos.Puedes abrir el asistente de checkin " - f" para completar los datos.", + f"por registrar sus datos.Puedes abrir el asistente de checkin " + f" para completar los datos.", "priority": 530, }, { "code": "checkin_no_precheckin", "title": "Entrada Hoy", "domain": "[('state', '=', 'confirm')," - f"('checkin', '=', '{today}')," - "('pending_checkin_data', '>', 0)," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + f"('checkin', '=', '{today}')," + "('pending_checkin_data', '>', 0)," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: all([c.state in ('draft','dummy') for c in r.checkin_partner_ids]) ", "text": "Registra los datos de los huéspedes desde el asistente del checkin.", "priority": 580, @@ -1221,24 +1225,24 @@ class PmsReservationService(Component): "code": "confirmed_without_payment_and_precheckin", "title": "Confirmadas a futuro sin pagar y sin precheckin realizado", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('reservation_type', 'in', ['normal', 'staff'])," - f"('checkin', '>', '{today}')," - "('pending_checkin_data', '>', 0)," - "('folio_payment_state', 'in', ['not_paid', 'partial'])" - "]", + "('reservation_type', 'in', ['normal', 'staff'])," + f"('checkin', '>', '{today}')," + "('pending_checkin_data', '>', 0)," + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", "text": "Esta reserva está pendiente de cobro y de que los huéspedes " - " registren sus datos: puedes enviarles un recordatorio desde aquí", + " registren sus datos: puedes enviarles un recordatorio desde aquí", "priority": 600, }, { "code": "confirmed_without_payment", "title": "Confirmadas a futuro sin pagar", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('reservation_type', 'in', ['normal', 'staff'])," - f"('checkin', '>', '{today}')," - "('pending_checkin_data', '=', 0)," - "('folio_payment_state', 'in', ['not_paid', 'partial'])" - "]", + "('reservation_type', 'in', ['normal', 'staff'])," + f"('checkin', '>', '{today}')," + "('pending_checkin_data', '=', 0)," + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", "text": "Esta reserva está pendiente de cobro, puedes enviarle sun recordatorio desde aquí", "priority": 630, }, @@ -1246,11 +1250,11 @@ class PmsReservationService(Component): "code": "confirmed_without_precheckin", "title": "Confirmadas a futuro sin pagar", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('reservation_type', 'in', ['normal', 'staff'])," - f"('checkin', '>', '{today}')," - "('pending_checkin_data', '>', 0)," - "('folio_payment_state', 'in', ['paid', 'overpayment','nothing_to_pay'])" - "]", + "('reservation_type', 'in', ['normal', 'staff'])," + f"('checkin', '>', '{today}')," + "('pending_checkin_data', '>', 0)," + "('folio_payment_state', 'in', ['paid', 'overpayment','nothing_to_pay'])" + "]", "text": "Esta reserva no tiene los datos de los huéspedes registrados, puedes enviarles un recordatorio desde aquí", "priority": 660, }, @@ -1258,20 +1262,20 @@ class PmsReservationService(Component): "code": "cancelled", "title": "Cancelada con cargos y sin cobrar", "domain": "[('state', '=', 'cancel')," - "('cancelled_reason', 'in',['late','noshow'])," - "('folio_payment_state', 'in', ['not_paid', 'partial'])," - "]", + "('cancelled_reason', 'in',['late','noshow'])," + "('folio_payment_state', 'in', ['not_paid', 'partial'])," + "]", "filtered": "lambda r: r.service_ids.filtered(lambda s: s.is_cancel_penalty and s.price_total > 0)", "text": f"La reserva de {reservation.partner_name} ha sido cancelada con una penalización de {reservation.service_ids.filtered(lambda s: s.is_cancel_penalty).price_total}€," - " puedes eliminar la penalización en caso de que no se vaya a cobrar.", + " puedes eliminar la penalización en caso de que no se vaya a cobrar.", "priority": 700, }, { "code": "onboard_without_payment", "title": "Por cobrar dentro", "domain": "[('state', 'in', ['onboard', 'departure_delayed'])," - "('folio_payment_state', 'in', ['not_paid', 'partial'])" - "]", + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", "text": f"En esta reserva tenemos un pago pendiente de {reservation.folio_pending_amount}. Puedes registrar el pago desde aquí.", "priority": 800, }, @@ -1279,37 +1283,37 @@ class PmsReservationService(Component): "code": "done_without_payment", "title": "Por cobrar pasadas", "domain": "[('state', '=', 'done')," - "('folio_payment_state', 'in', ['not_paid', 'partial'])" - "]", + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", "text": f"Esta reserva ha quedado con un cargo pendiente de {reservation.folio_pending_amount}€." - " Cuando gestiones el cobro puedes registrarlo desde aquí.", + " Cuando gestiones el cobro puedes registrarlo desde aquí.", "priority": 900, }, { - "code":"checkout", + "code": "checkout", "title": "Checkout", "domain": "[('state', 'in', ['onboard', 'departure_delayed'])," - f"('checkout', '=', '{today}')," - "]", + f"('checkout', '=', '{today}')," + "]", "text": "Reserva lista para el checkout, marca la salida directamente desde aquí.", "priority": 1000, }, ] - # We order the states by priority and return the first state whose domain meets the reservation; - # if the state also has the key 'filtered,' it must also meet that filter. + # We order the states by priority and return the first + # state whose domain meets the reservation; + # if the state also has the key 'filtered,' + # it must also meet that filter. - sorted_wizard_states = sorted(wizard_states, key=lambda x: x['priority']) + sorted_wizard_states = sorted(wizard_states, key=lambda x: x["priority"]) PmsWizardStateInfo = self.env.datamodels["pms.wizard.state.info"] for state in sorted_wizard_states: - domain = expression.AND([ - [("id", "=", reservation_id)], - safe_eval(state["domain"]) - ]) + domain = expression.AND( + [[("id", "=", reservation_id)], safe_eval(state["domain"])] + ) if self.env["pms.reservation"].search_count(domain): - if ( - state.get("filtered") - and not self.env["pms.reservation"].browse(reservation_id).filtered(safe_eval(state["filtered"])) - ): + if state.get("filtered") and not self.env["pms.reservation"].browse( + reservation_id + ).filtered(safe_eval(state["filtered"])): continue return PmsWizardStateInfo( @@ -1319,8 +1323,7 @@ class PmsReservationService(Component): ) return PmsWizardStateInfo( - code='', - title='', - text='', - ) - + code="", + title="", + text="", + )