diff --git a/pms_api_rest/README.rst b/pms_api_rest/README.rst new file mode 100644 index 0000000000..e3891ca231 --- /dev/null +++ b/pms_api_rest/README.rst @@ -0,0 +1,77 @@ +============ +PMS API REST +============ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpms-lightgray.png?logo=github + :target: https://github.com/OCA/pms/tree/14.0/pms_housekeeping + :alt: OCA/pms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pms-14-0/pms-14-0-pms_housekeeping + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/293/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds an API REST feature to property management system (PMS). + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Commit [Sun] + +Contributors +~~~~~~~~~~~~ + +* `Commit [Sun] `: + + * Sara Lago + * Miguel Padín + + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/pms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pms_api_rest/__init__.py b/pms_api_rest/__init__.py new file mode 100644 index 0000000000..e95aba4aba --- /dev/null +++ b/pms_api_rest/__init__.py @@ -0,0 +1,5 @@ +from . import controllers +from . import datamodels +from . import services +from . import models +from . import http diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py new file mode 100644 index 0000000000..73f298c565 --- /dev/null +++ b/pms_api_rest/__manifest__.py @@ -0,0 +1,39 @@ +{ + "name": "API REST PMS", + "author": "Commit [Sun], Odoo Community Association (OCA)", + "website": "https://github.com/OCA/pms", + "category": "Generic Modules/Property Management System", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "depends": [ + "pms", + "base_rest", + "base_rest_datamodel", + "web", + "auth_signup", + "auth_jwt_login", + "base_location", + "l10n_es_aeat", + "sql_export_excel", + "feed_rss", + ], + "external_dependencies": { + "python": ["jwt", "simplejson", "marshmallow", "jose"], + }, + "data": [ + "security/ir.model.access.csv", + "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", + "views/product_template_views.xml", + "views/pms_api_log_views.xml", + ], + "demo": [ + "demo/pms_api_rest_master_data.xml", + ], + "installable": True, +} diff --git a/pms_api_rest/controllers/__init__.py b/pms_api_rest/controllers/__init__.py new file mode 100644 index 0000000000..5e366b40cb --- /dev/null +++ b/pms_api_rest/controllers/__init__.py @@ -0,0 +1 @@ +from . import pms_rest diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py new file mode 100644 index 0000000000..366984bbe3 --- /dev/null +++ b/pms_api_rest/controllers/pms_rest.py @@ -0,0 +1,9 @@ +from odoo.addons.base_rest.controllers import main + + +class BaseRestPrivateApiController(main.RestController): + _root_path = "/api/" + _collection_name = "pms.services" + _default_auth = "public" + _default_save_session = False + _default_cors = "*" diff --git a/pms_api_rest/data/auth_jwt_validator.xml b/pms_api_rest/data/auth_jwt_validator.xml new file mode 100644 index 0000000000..5b7b1c6266 --- /dev/null +++ b/pms_api_rest/data/auth_jwt_validator.xml @@ -0,0 +1,14 @@ + + + api_pms + api_pms + pms + secret + HS256 + pms_secret_key_example + login + 1 + email + + + diff --git a/pms_api_rest/data/cron_jobs.xml b/pms_api_rest/data/cron_jobs.xml new file mode 100644 index 0000000000..650b367899 --- /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/data/pms_app_reset_password_template.xml b/pms_api_rest/data/pms_app_reset_password_template.xml new file mode 100644 index 0000000000..82b340c662 --- /dev/null +++ b/pms_api_rest/data/pms_app_reset_password_template.xml @@ -0,0 +1,147 @@ + + + + Pms Reset Password + + Restablecer Contraseña + "${object.company_id.name | safe}" <${(object.company_id.email or user.email) | safe}> + ${object.email_formatted | safe} + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + +
+ + ROOMDOO + +
+
+ + +
+ + ${object.name} + + + ${object.company_id.name} +
+
+ + +
+
+ A password reset was requested for the Odoo account linked to this email. + You may change your password by following this link which will remain valid during 15 minutes: +
+ + If you do not expect this, you can safely ignore this email.

+ Thanks, + % if user.signature: +
+ ${user.signature | safe} + % endif +
+
+
+ + + +
+ ${object.company_id.name} +
+ ${object.company_id.phone} + % if object.company_id.email + | ${object.company_id.email} + % endif + % if object.company_id.website + | + ${object.company_id.website} + + % endif +
+
+
+
+ ${object.lang} + +
+
diff --git a/pms_api_rest/data/sql_reports.xml b/pms_api_rest/data/sql_reports.xml new file mode 100644 index 0000000000..099b967489 --- /dev/null +++ b/pms_api_rest/data/sql_reports.xml @@ -0,0 +1,143 @@ + + + + x_date_from + Date + date + + sql.file.wizard + manual + + + + x_date_to + Date + date + + sql.file.wizard + manual + + + + x_pms_property_id + Property + integer + + sql.file.wizard + manual + + + + + Export Departures + excel + + SELECT + TO_CHAR(reservation.checkout, 'DD-MM-YYYY') as "Departure", + folio.name as "Reservation", + room.name as "Room", + reservation.partner_name as "Customer", + folio.pending_amount as "Pending Amount" + FROM pms_reservation reservation + LEFT JOIN pms_reservation_line night + ON reservation.id = night.reservation_id AND night.date = reservation.checkout - interval '1' day + LEFT JOIN pms_room room + ON room.id = night.room_id + LEFT JOIN pms_folio folio + ON folio.id = reservation.folio_id + WHERE (reservation.pms_property_id = %(x_pms_property_id)s) + AND (reservation.checkout = %(x_date_from)s) + AND (night.occupies_availability = True) + ORDER BY reservation.name + + + + + + + + + Export Arrivals + excel + + SELECT + TO_CHAR(reservation.checkin, 'DD-MM-YYYY') as "Arrival", + folio.name as "Reservation", + room.name as "Room", + reservation.partner_name as "Customer", + folio.pending_amount as "Pending Amount" + FROM pms_reservation reservation + LEFT JOIN pms_reservation_line night + ON reservation.id = night.reservation_id AND night.date = reservation.checkin + LEFT JOIN pms_room room + ON room.id = night.room_id + LEFT JOIN pms_folio folio + ON folio.id = reservation.folio_id + WHERE (reservation.pms_property_id = %(x_pms_property_id)s) + AND (reservation.checkin = %(x_date_from)s) + AND (night.occupies_availability = True) + ORDER BY reservation.name + + + + + + + + + Export Services + excel + + SELECT + TO_CHAR(line.date, 'DD-MM-YYYY') as "Date", + reservation.name as "Reservation", + reservation.rooms, + product_tmpl.name as "Name", + line.day_qty as "Units", + reservation.adults as "Room Adults", + reservation.children as "Room Childrens", + line.is_board_service as "Board Service", + reservation.partner_name as "Partner name", + line.price_unit as "Precio" + FROM pms_service_line line + LEFT JOIN product_product product + ON line.product_id = product.id + LEFT JOIN product_template product_tmpl + ON product.product_tmpl_id = product_tmpl.id + LEFT JOIN pms_reservation reservation + ON line.reservation_id = reservation.id + LEFT JOIN pms_checkin_partner room_host + ON room_host.reservation_id = reservation.id + WHERE (line.date >= %(x_date_from)s) + AND (line.date <= %(x_date_to)s) + AND (line.pms_property_id = %(x_pms_property_id)s) + AND (reservation.state != 'cancel') + GROUP BY line.id, product_tmpl.name, reservation.name, reservation.rooms, reservation.adults, reservation.children, reservation.partner_name, line.price_unit + ORDER BY date asc + + + + + + diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py new file mode 100644 index 0000000000..ce0f40d890 --- /dev/null +++ b/pms_api_rest/datamodels/__init__.py @@ -0,0 +1,68 @@ +from . import pms_rest_metadata +from . import pms_calendar + +from . import pms_folio + +from . import pms_room +from . import pms_room_type +from . import pms_room_type_class + +from . import pms_reservation +from . import pms_reservation_line + +from . import pms_checkin_partner +from . import pms_partner + +from . import pms_property +from . import pms_account_journal +from . import pms_transaction +from . import pms_invoice + +from . import pms_user + +from . import pms_pricelist +from . import pms_pricelist_item +from . import pms_price +from . import pms_availability_plan +from . import pms_availability_plan_rule + +from . import pms_id_category +from . import res_country +from . import res_partner_category +from . import res_city_zip + +from . import pms_search_param +from . import pms_ubication +from . import pms_extra_bed + +from . import pms_amenity_type +from . import pms_amenity + +from . import pms_board_service +from . import pms_board_service_line + +from . import pms_product +from . import pms_sale_channel +from . import pms_cancelation_rule +from . import pms_agency +from . import pms_service +from . import pms_service_line +from . import res_users +from . import res_lang +from . import pms_account_payment_term + +from . import pms_room_closure_reason +from . import pms_cash_register +from . import pms_report +from . import pms_folio_sale_line +from . import pms_invoice_line +from . import pms_mail +from . import pms_notification +from . import pms_reservation_message +from . import pms_avail +from . import pms_dashboard +from . import feed_post + +from . import pms_wizard_state + +from . import pms_ocr diff --git a/pms_api_rest/datamodels/feed_post.py b/pms_api_rest/datamodels/feed_post.py new file mode 100644 index 0000000000..b6e2861a7c --- /dev/null +++ b/pms_api_rest/datamodels/feed_post.py @@ -0,0 +1,14 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class FeedPost(Datamodel): + _name = "feed.post.info" + postId = fields.String(required=True, allow_none=False) + title = fields.String(required=True, allow_none=False) + link = fields.String(required=True, allow_none=False) + description = fields.String(required=True, allow_none=False) + publishDate = fields.String(required=True, allow_none=False) + author = fields.String(required=True, allow_none=False) + imageUrl = fields.String(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_account_journal.py b/pms_api_rest/datamodels/pms_account_journal.py new file mode 100644 index 0000000000..6b9f4c5128 --- /dev/null +++ b/pms_api_rest/datamodels/pms_account_journal.py @@ -0,0 +1,16 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAccountJournalSearchParam(Datamodel): + _name = "pms.account.journal.search.param" + pmsPropertyId = fields.Integer(required=False, allow_none=False) + + +class PmsAccountJournalInfo(Datamodel): + _name = "pms.account.journal.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + type = fields.String(required=False, allow_none=True) + allowedPayments = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_account_payment_term.py b/pms_api_rest/datamodels/pms_account_payment_term.py new file mode 100644 index 0000000000..74e15fb1b9 --- /dev/null +++ b/pms_api_rest/datamodels/pms_account_payment_term.py @@ -0,0 +1,9 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAccountTransactiontTermInfo(Datamodel): + _name = "pms.account.transaction.term.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_agency.py b/pms_api_rest/datamodels/pms_agency.py new file mode 100644 index 0000000000..1c519997d2 --- /dev/null +++ b/pms_api_rest/datamodels/pms_agency.py @@ -0,0 +1,16 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAgencySearchParam(Datamodel): + _name = "pms.agency.search.param" + name = fields.String(required=False, allow_none=True) + otas = fields.Boolean(required=False, allow_none=True) + + +class PmsAgencyInfo(Datamodel): + _name = "pms.agency.info" + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) + imageUrl = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_amenity.py b/pms_api_rest/datamodels/pms_amenity.py new file mode 100644 index 0000000000..b36ccdae9c --- /dev/null +++ b/pms_api_rest/datamodels/pms_amenity.py @@ -0,0 +1,18 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAmenitySearchParam(Datamodel): + _name = "pms.amenity.search.param" + name = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + + +class PmsAmenityInfo(Datamodel): + _name = "pms.amenity.info" + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) + defaultCode = fields.String(required=False, allow_none=True) + amenityTypeId = fields.Integer(required=False, allow_none=True) + addInRoomName = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_amenity_type.py b/pms_api_rest/datamodels/pms_amenity_type.py new file mode 100644 index 0000000000..86af7e082d --- /dev/null +++ b/pms_api_rest/datamodels/pms_amenity_type.py @@ -0,0 +1,15 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAmenityTypeSearchParam(Datamodel): + _name = "pms.amenity.type.search.param" + name = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + + +class PmsAmenityTypeInfo(Datamodel): + _name = "pms.amenity.type.info" + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_avail.py b/pms_api_rest/datamodels/pms_avail.py new file mode 100644 index 0000000000..4337400507 --- /dev/null +++ b/pms_api_rest/datamodels/pms_avail.py @@ -0,0 +1,20 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAvailSearchParam(Datamodel): + _name = "pms.avail.search.param" + availabilityFrom = fields.String(required=True, allow_none=True) + availabilityTo = fields.String(required=True, allow_none=True) + pmsPropertyId = fields.Integer(required=True, allow_none=True) + pricelistId = fields.Integer(required=False, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + realAvail = fields.Boolean(required=False, allow_none=True) + currentLines = fields.List(fields.Integer(), required=False, allow_none=False) + + +class PmsAvailInfo(Datamodel): + _name = "pms.avail.info" + date = fields.String(required=True, allow_none=False) + roomIds = fields.List(fields.Integer, required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_availability_plan.py b/pms_api_rest/datamodels/pms_availability_plan.py new file mode 100644 index 0000000000..c0efee5fdd --- /dev/null +++ b/pms_api_rest/datamodels/pms_availability_plan.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAvailabilityPlanInfo(Datamodel): + _name = "pms.availability.plan.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(required=False, allow_none=True)) diff --git a/pms_api_rest/datamodels/pms_availability_plan_rule.py b/pms_api_rest/datamodels/pms_availability_plan_rule.py new file mode 100644 index 0000000000..632ba300d7 --- /dev/null +++ b/pms_api_rest/datamodels/pms_availability_plan_rule.py @@ -0,0 +1,34 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsAvailabilityPlanRuleSearchParam(Datamodel): + _name = "pms.availability.plan.rule.search.param" + dateFrom = fields.String(required=False, allow_none=False) + dateTo = fields.String(required=False, allow_none=False) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + + +class PmsAvailabilityPlanRuleInfo(Datamodel): + _name = "pms.availability.plan.rule.info" + availabilityRuleId = fields.Integer(required=False, allow_none=True) + minStay = fields.Integer(required=False, allow_none=True) + minStayArrival = fields.Integer(required=False, allow_none=True) + maxStay = fields.Integer(required=False, allow_none=True) + maxStayArrival = fields.Integer(required=False, allow_none=True) + closed = fields.Boolean(required=False, allow_none=True) + closedDeparture = fields.Boolean(required=False, allow_none=True) + closedArrival = fields.Boolean(required=False, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + quota = fields.Integer(required=False, allow_none=True) + maxAvailability = fields.Integer(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + availabilityPlanId = fields.Integer(required=False, allow_none=True) + + +class PmsAvailabilityPlanRulesInfo(Datamodel): + _name = "pms.availability.plan.rules.info" + availabilityPlanRules = fields.List(NestedModel("pms.availability.plan.rule.info")) diff --git a/pms_api_rest/datamodels/pms_board_service.py b/pms_api_rest/datamodels/pms_board_service.py new file mode 100644 index 0000000000..a74b0198aa --- /dev/null +++ b/pms_api_rest/datamodels/pms_board_service.py @@ -0,0 +1,21 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsBoardServiceSearchParam(Datamodel): + _name = "pms.board.service.search.param" + name = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + + +class PmsBoardServiceInfo(Datamodel): + _name = "pms.board.service.info" + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) + roomTypeId = fields.Integer(required=True, allow_none=False) + amount = fields.Float(required=False, allow_none=False) + boardServiceId = fields.Integer(required=False, allow_none=False) + productIds = fields.List(fields.Integer(required=False, allow_none=False)) + boardServiceLineIds = fields.List(fields.Integer(required=False, allow_none=False)) diff --git a/pms_api_rest/datamodels/pms_board_service_line.py b/pms_api_rest/datamodels/pms_board_service_line.py new file mode 100644 index 0000000000..6c56acbc7e --- /dev/null +++ b/pms_api_rest/datamodels/pms_board_service_line.py @@ -0,0 +1,19 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsBoardServiceLineSearchParam(Datamodel): + _name = "pms.board.service.line.search.param" + boardServiceId = fields.Integer(required=True, allow_none=False) + + +class PmsBoardServiceLineInfo(Datamodel): + _name = "pms.board.service.line.info" + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) + boardServiceId = fields.Integer(required=True, allow_none=False) + productId = fields.Integer(required=True, allow_none=False) + amount = fields.Float(required=False, allow_none=False) + isAdults = fields.Boolean(required=False, allow_none=False) + isChildren = fields.Boolean(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py new file mode 100644 index 0000000000..a7bfb98267 --- /dev/null +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -0,0 +1,112 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsReservationUpdates(Datamodel): + _name = "pms.reservation.updates" + reservationLinesChanges = fields.List(fields.Dict(required=False, allow_none=True)) + preferredRoomId = fields.Integer(required=False, allow_none=True) + boardServiceId = fields.Integer(required=False, allow_none=True) + pricelistId = fields.Integer(required=False, allow_none=True) + adults = fields.Integer(required=False, allow_none=True) + children = fields.Integer(required=False, allow_none=True) + segmentationId = fields.Integer(required=False, allow_none=True) + + +class PmsCalendarSwapInfo(Datamodel): + _name = "pms.calendar.swap.info" + pmsPropertyId = fields.Integer(required=True, allow_none=False) + roomId = fields.Integer(required=True, allow_none=False) + date = fields.String(required=True, allow_none=False) + reservationLineIds = fields.List(fields.Integer(required=True, allow_none=False)) + + +class PmsCalendarSearchParam(Datamodel): + _name = "pms.calendar.search.param" + dateFrom = fields.String(required=False, allow_none=True) + dateTo = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + pricelistId = fields.Integer(required=False, allow_none=True) + availabilityPlanId = fields.Integer(required=False, allow_none=True) + + +class PmsCalendarHeaderSearchParam(Datamodel): + _name = "pms.calendar.header.search.param" + dateFrom = fields.String(required=False, allow_none=True) + dateTo = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + roomIds = fields.List(fields.Integer(), required=False) + + +class PmsCalendarFreeDailyRoomsByType(Datamodel): + _name = "pms.calendar.free.daily.rooms.by.type" + date = fields.String(required=True, allow_none=False) + roomTypeId = fields.Integer(required=True, allow_none=False) + freeRooms = fields.Integer(required=True, allow_none=False) + + +class PmsCalendarDailyInvoicing(Datamodel): + _name = "pms.calendar.daily.invoicing" + date = fields.String(required=True, allow_none=False) + invoicingTotal = fields.Float(required=True, allow_none=False) + + +class PmsCalendarHeaderInfo(Datamodel): + _name = "pms.calendar.header.info" + date = fields.String(required=True, allow_none=False) + dailyBilling = fields.Float(required=True, allow_none=False) + freeRooms = fields.Integer(required=True, allow_none=False) + occupancyRate = fields.Float(required=True, allow_none=False) + overbooking = fields.Boolean(required=False, allow_none=True) + + +class PmsCalendarInfo(Datamodel): + _name = "pms.calendar.info" + id = fields.Integer(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + roomId = fields.Integer(required=False, allow_none=True) + roomTypeName = fields.String(required=False, allow_none=True) + toAssign = fields.Boolean(required=False, allow_none=True) + splitted = fields.Boolean(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + partnerName = fields.String(required=False, allow_none=True) + folioId = fields.Integer(required=False, allow_none=True) + reservationId = fields.Integer(required=False, allow_none=True) + reservationName = fields.String(required=False, allow_none=True) + reservationType = fields.String(required=False, allow_none=True) + isFirstNight = fields.Boolean(required=False, allow_none=True) + isLastNight = fields.Boolean(required=False, allow_none=True) + totalPrice = fields.Float(required=False, allow_none=True) + pendingPayment = fields.Float(required=False, allow_none=True) + numNotifications = fields.Integer(required=False, allow_none=True) + adults = fields.Integer(required=False, allow_none=True) + children = fields.Integer(required=False, allow_none=True) + nextLineSplitted = fields.Boolean(required=False, allow_none=True) + previousLineSplitted = fields.Boolean(required=False, allow_none=True) + closureReasonId = fields.Number(required=False, allow_none=True) + priceDayTotal = fields.Number(required=False, allow_none=True) + priceDayTotalServices = fields.Number(required=False, allow_none=True) + isReselling = fields.Boolean(required=False, allow_none=False) + + +class PmsCalendarRenderInfo(Datamodel): + _name = "pms.calendar.render.info" + roomId = fields.Integer(required=True, allow_none=False) + capacity = fields.Integer(required=True, allow_none=False) + roomTypeClassId = fields.Integer(required=True, allow_none=False) + roomTypeId = fields.Integer(required=True, allow_none=False) + dates = fields.List(fields.Dict(required=True, allow_none=False)) + + +class PmsCalendarPricesRulesRenderInfo(Datamodel): + _name = "pms.calendar.prices.rules.render.info" + roomTypeId = fields.Integer(required=True, allow_none=False) + dates = fields.List(fields.Dict(required=True, allow_none=False)) + + +class PmsCalendarAlertsPerDay(Datamodel): + _name = "pms.calendar.alerts.per.day" + date = fields.String(required=True, allow_none=False) + overbooking = fields.Boolean(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_cancelation_rule.py b/pms_api_rest/datamodels/pms_cancelation_rule.py new file mode 100644 index 0000000000..a2959dfe77 --- /dev/null +++ b/pms_api_rest/datamodels/pms_cancelation_rule.py @@ -0,0 +1,15 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsCancelationRuleSearchParam(Datamodel): + _name = "pms.cancelation.rule.search.param" + pricelistId = fields.Integer(required=False, allow_none=True) + pmsPropertyId = fields.String(required=False, allow_none=True) + + +class PmsCancelationRuleInfo(Datamodel): + _name = "pms.cancelation.rule.info" + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_cash_register.py b/pms_api_rest/datamodels/pms_cash_register.py new file mode 100644 index 0000000000..672639e027 --- /dev/null +++ b/pms_api_rest/datamodels/pms_cash_register.py @@ -0,0 +1,32 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsCashRegisterInfo(Datamodel): + _name = "pms.cash.register.info" + id = fields.Integer(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + userId = fields.Integer(required=False, allow_none=True) + balance = fields.Float(required=False, allow_none=True) + dateTime = fields.String(required=False, allow_none=True) + + +class PmsCashRegisterSearchParam(Datamodel): + _name = "pms.cash.register.search.param" + journalId = fields.Integer(required=False, allow_none=True) + + +class PmsCashRegisterAction(Datamodel): + _name = "pms.cash.register.action" + action = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + amount = fields.Float(required=False, allow_none=True) + journalId = fields.Integer(required=False, allow_none=True) + forceAction = fields.Boolean(required=False, allow_none=True) + + +class PmsCashRegisterResult(Datamodel): + _name = "pms.cash.register.result" + result = fields.Boolean(required=False, allow_none=False) + diff = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py new file mode 100644 index 0000000000..c2b8ecb240 --- /dev/null +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -0,0 +1,38 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsCheckinPartnerInfo(Datamodel): + _name = "pms.checkin.partner.info" + id = fields.Integer(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + reservationId = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + firstname = fields.String(required=False, allow_none=True) + lastname = fields.String(required=False, allow_none=True) + lastname2 = fields.String(required=False, allow_none=True) + email = fields.String(required=False, allow_none=True) + mobile = fields.String(required=False, allow_none=True) + documentLegalRepresentative = fields.String(required=False, allow_none=True) + relationship = fields.String(required=False, allow_none=True) + responsibleCheckinPartnerId = fields.Integer(required=False, allow_none=True) + documentType = fields.Integer(required=False, allow_none=True) + documentNumber = fields.String(required=False, allow_none=True) + documentExpeditionDate = fields.String(required=False, allow_none=True) + documentSupportNumber = fields.String(required=False, allow_none=True) + documentCountryId = fields.Integer(required=False, allow_none=True) + gender = fields.String(required=False, allow_none=True) + birthdate = fields.String(required=False, allow_none=True) + residenceStreet = fields.String(required=False, allow_none=True) + zip = fields.String(required=False, allow_none=True) + residenceCity = fields.String(required=False, allow_none=True) + nationality = fields.Integer(required=False, allow_none=True) + countryState = fields.Integer(required=False, allow_none=True) + countryStateName = fields.String(required=False, allow_none=True) + countryId = fields.Integer(required=False, allow_none=True) + checkinPartnerState = fields.String(required=False, allow_none=True) + actionOnBoard = fields.Boolean(required=False, allow_none=True) + originInputData = fields.String(required=False, allow_none=True) + signature = fields.String(required=False, allow_none=True) + isAlreadyInReservation = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_dashboard.py b/pms_api_rest/datamodels/pms_dashboard.py new file mode 100644 index 0000000000..bbc011cc24 --- /dev/null +++ b/pms_api_rest/datamodels/pms_dashboard.py @@ -0,0 +1,52 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsDashboardSearchParam(Datamodel): + _name = "pms.dashboard.search.param" + date = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + + +class PmsDashboardRangeDatesSearchParam(Datamodel): + _name = "pms.dashboard.range.dates.search.param" + dateFrom = fields.String(required=False, allow_none=True) + dateTo = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + + +class PmsDashboardPendingReservations(Datamodel): + _name = "pms.dashboard.pending.reservations" + date = fields.String(required=False, allow_none=True) + pendingArrivalReservations = fields.Integer(required=False, allow_none=True) + completedArrivalReservations = fields.Integer(required=False, allow_none=True) + pendingDepartureReservations = fields.Integer(required=False, allow_none=True) + completedDepartureReservations = fields.Integer(required=False, allow_none=True) + + +class PmsDashboardStateRooms(Datamodel): + _name = "pms.dashboard.state.rooms" + date = fields.String(required=False, allow_none=True) + numOccupiedRooms = fields.Integer(required=False, allow_none=True) + numFreeRooms = fields.Integer(required=False, allow_none=True) + numOutOfServiceRooms = fields.Integer(required=False, allow_none=True) + + +class PmsDashboardReservationsBySaleChannel(Datamodel): + _name = "pms.dashboard.reservations.by.sale.channel" + saleChannelName = fields.String(required=False, allow_none=True) + percentageReservationsSoldBySaleChannel = fields.Integer( + required=False, allow_none=True + ) + + +class PmsDashboardNumericResponse(Datamodel): + _name = "pms.dashboard.numeric.response" + value = fields.Float(required=False, allow_none=True) + + +class PmsDashboardDailyBilling(Datamodel): + _name = "pms.dashboard.daily.billing" + date = fields.String(required=False, allow_none=True) + billing = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_extra_bed.py b/pms_api_rest/datamodels/pms_extra_bed.py new file mode 100644 index 0000000000..7d63e8ee15 --- /dev/null +++ b/pms_api_rest/datamodels/pms_extra_bed.py @@ -0,0 +1,19 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsExtraBedSearchParam(Datamodel): + _name = "pms.extra.beds.search.param" + name = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + dateFrom = fields.String(required=False, allow_none=True) + dateTo = fields.String(required=False, allow_none=True) + + +class PmsExtraBedInfo(Datamodel): + _name = "pms.extra.bed.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + dailyLimitConfig = fields.Integer(required=False, allow_none=True) + dailyLimitAvail = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py new file mode 100644 index 0000000000..8adea6a2b4 --- /dev/null +++ b/pms_api_rest/datamodels/pms_folio.py @@ -0,0 +1,117 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsFolioSearchParam(Datamodel): + _name = "pms.folio.search.param" + _inherit = "pms.rest.metadata" + pmsPropertyId = fields.Integer(required=True, allow_none=True) + dateFrom = fields.String(required=False, allow_none=True) + dateTo = fields.String(required=False, allow_none=True) + filter = fields.String(required=False, allow_none=True) + filterByState = fields.String(required=False, allow_none=True) + last = fields.Boolean(required=False, allow_none=True) + ids = fields.List(fields.Integer(), required=False) + createDateFrom = fields.String(required=False, allow_none=True) + createDateTo = fields.String(required=False, allow_none=True) + lastUpdateFrom = fields.String(required=False, allow_none=True) + + +class PmsFolioInfo(Datamodel): + _name = "pms.folio.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + partnerName = fields.String(required=False, allow_none=True) + partnerPhone = fields.String(required=False, allow_none=True) + partnerEmail = fields.String(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + amountTotal = fields.Float(required=False, allow_none=True) + reservationType = fields.String(required=False, allow_none=True) + pendingAmount = fields.Float(required=False, allow_none=True) + firstCheckin = fields.String(required=False, allow_none=True) + lastCheckout = fields.String(required=False, allow_none=True) + createDate = fields.String(required=False, allow_none=True) + createdBy = fields.String(required=False, allow_none=True) + + pmsPropertyId = fields.Integer(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + reservations = fields.List( + NestedModel("pms.reservation.info"), required=False, allow_none=True + ) + pricelistId = fields.Integer(required=False, allow_none=True) + saleChannelId = fields.Integer(required=False, allow_none=True) + agencyId = fields.Integer(required=False, allow_none=True) + externalReference = fields.String(required=False, allow_none=True) + closureReasonId = fields.Integer(required=False, allow_none=True) + outOfServiceDescription = fields.String(required=False, allow_none=True) + preconfirm = fields.Boolean(required=False, allow_none=True) + internalComment = fields.String(required=False, allow_none=True) + # REVIEW: Mail workflow folio + sendConfirmationMail = fields.Boolean(required=False, allow_none=True) + cancelReservations = fields.Boolean(required=False, allow_none=True) + confirmReservations = fields.Boolean(required=False, allow_none=True) + invoiceStatus = fields.String(required=False, allow_none=True) + portalUrl = fields.String(required=False, allow_none=True) + accessToken = fields.String(required=False, allow_none=True) + language = fields.String(required=False, allow_none=True) + transactions = fields.List( + NestedModel("pms.transaction.info"), required=False, allow_none=True + ) + + +class PmsFolioShortInfo(Datamodel): + _name = "pms.folio.short.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + partnerName = fields.String(required=False, allow_none=True) + partnerPhone = fields.String(required=False, allow_none=True) + partnerEmail = fields.String(required=False, allow_none=True) + amountTotal = fields.Float(required=False, allow_none=True) + pendingAmount = fields.Float(required=False, allow_none=True) + paymentStateCode = fields.String(required=False, allow_none=True) + paymentStateDescription = fields.String(required=False, allow_none=True) + reservations = fields.List(fields.Dict(required=False, allow_none=True)) + numReservations = fields.Integer(required=False, allow_none=True) + reservationType = fields.String(required=False, allow_none=True) + closureReasonId = fields.Integer(required=False, allow_none=True) + agencyId = fields.Integer(required=False, allow_none=True) + pricelistId = fields.Integer(required=False, allow_none=True) + saleChannelId = fields.Integer(required=False, allow_none=True) + firstCheckin = fields.String(required=False, allow_none=True) + lastCheckout = fields.String(required=False, allow_none=True) + createDate = fields.String(required=False, allow_none=True) + createHour = fields.String(required=False, allow_none=True) + + +class PmsFolioPublicInfo(Datamodel): + _name = "pms.folio.public.info" + pmsCompanyName = fields.String(required=False, allow_none=True) + pmsPropertyName = fields.String(required=False, allow_none=True) + pmsPropertyStreet = fields.String(required=False, allow_none=True) + pmsPropertyCity = fields.String(required=False, allow_none=True) + pmsPropertyState = fields.String(required=False, allow_none=True) + pmsPropertyZip = fields.String(required=False, allow_none=True) + pmsPropertyPhoneNumber = fields.String(required=False, allow_none=True) + pmsPropertyLogo = fields.String(required=False, allow_none=True) + pmsPropertyImage = fields.String(required=False, allow_none=True) + pmsPropertyIneCategory = fields.String(required=False, allow_none=True) + pmsPropertyPrivacyPolicy = fields.String(required=False, allow_none=True) + pmsPropertyIsOCRAvailable = fields.Boolean(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + folioPartnerName = fields.String(required=False, allow_none=True) + folioRoomTypesDescription = fields.String(required=False, allow_none=True) + folioReference = fields.String(required=False, allow_none=True) + folioPaymentLink = fields.String(required=False, allow_none=True) + folioPortalLink = fields.String(required=False, allow_none=True) + folioPendingAmount = fields.Float(required=False, allow_none=True) + folioNumCheckins = fields.Integer(required=False, allow_none=True) + folioCheckinNamesCompleted = fields.List( + fields.String(required=False, allow_none=True) + ) + reservations = fields.List( + NestedModel("pms.reservation.public.info"), required=True, allow_none=False + ) + cardexWarning = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_folio_sale_line.py b/pms_api_rest/datamodels/pms_folio_sale_line.py new file mode 100644 index 0000000000..1759f51f8d --- /dev/null +++ b/pms_api_rest/datamodels/pms_folio_sale_line.py @@ -0,0 +1,20 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsFolioSaleInfo(Datamodel): + _name = "pms.folio.sale.line.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + priceUnit = fields.Float(required=False, allow_none=True) + qtyToInvoice = fields.Float(required=False, allow_none=True) + qtyInvoiced = fields.Float(required=False, allow_none=True) + priceTotal = fields.Float(required=False, allow_none=True) + discount = fields.Float(required=False, allow_none=True) + productQty = fields.Float(required=False, allow_none=True) + reservationId = fields.Integer(required=False, allow_none=True) + serviceId = fields.Integer(required=False, allow_none=True) + displayType = fields.String(required=False, allow_none=True) + defaultInvoiceTo = fields.Integer(required=False, allow_none=True) + isDownPayment = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_id_category.py b/pms_api_rest/datamodels/pms_id_category.py new file mode 100644 index 0000000000..eeb1ab43b3 --- /dev/null +++ b/pms_api_rest/datamodels/pms_id_category.py @@ -0,0 +1,11 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsIdCategoryInfo(Datamodel): + _name = "pms.id.category.info" + id = fields.Integer(required=False, allow_none=True) + documentType = fields.String(required=False, allow_none=True) + code = fields.String(required=False, allow_none=True) + countryIds = fields.List(fields.Integer(), required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_invoice.py b/pms_api_rest/datamodels/pms_invoice.py new file mode 100644 index 0000000000..1d36f100e1 --- /dev/null +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -0,0 +1,63 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsInvoiceSearchParam(Datamodel): + _name = "pms.invoice.search.param" + _inherit = "pms.rest.metadata" + id = fields.Integer(required=False, allow_none=True) + filter = fields.String(required=False, allow_none=True) + originAgencyId = fields.Integer(required=False, allow_none=True) + paymentState = fields.String(required=False, allow_none=True) + dateStart = fields.String(required=False, allow_none=True) + dateEnd = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + + +class PmsAccountInvoiceInfo(Datamodel): + _name = "pms.invoice.info" + id = fields.Integer(required=False, allow_none=True) + amount = fields.Float(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + paymentState = fields.String(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + # REVIEW: partnerName??, is not enought partnerId? + partnerName = fields.String(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + moveLines = fields.List( + NestedModel("pms.invoice.line.info"), required=False, allow_none=True + ) + folioId = fields.Integer(required=False, allow_none=True) + saleLines = fields.List(NestedModel("pms.folio.sale.line.info")) + narration = fields.String(required=False, allow_none=True) + portalUrl = fields.String(required=False, allow_none=True) + moveType = fields.String(required=False, allow_none=True) + isReversed = fields.Boolean(required=False, allow_none=True) + isDownPaymentInvoice = fields.Boolean(required=False, allow_none=True) + isSimplifiedInvoice = fields.Boolean(required=False, allow_none=True) + reversedEntryId = fields.Integer(required=False, allow_none=True) + # REVIEW: originDownPaymentId Only input field to service to + # create downpayment invoices from payments + originDownPaymentId = fields.Integer(required=False, allow_none=True) + originAgencyId = fields.Integer(required=False, allow_none=True) + ref = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + + +class PmsInvoiceResults(Datamodel): + _name = "pms.invoice.results" + invoices = fields.List(NestedModel("pms.invoice.info")) + total = fields.Float(required=False, allow_none=True) + totalInvoices = fields.Integer(required=False, allow_none=True) + + +class PmsAccountSendSearchParam(Datamodel): + _name = "pms.account.send.search.param" + invoiceIds = fields.List(fields.Integer(), required=False, allow_none=True) + partnerIds = fields.List(fields.Integer(), required=False, allow_none=True) + emailAddresses = fields.List(fields.String(), required=False, allow_none=True) + isEmail = fields.Boolean(required=False, allow_none=True) + isPrint = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_invoice_line.py b/pms_api_rest/datamodels/pms_invoice_line.py new file mode 100644 index 0000000000..374fd375a4 --- /dev/null +++ b/pms_api_rest/datamodels/pms_invoice_line.py @@ -0,0 +1,16 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsInvoiceLineInfo(Datamodel): + _name = "pms.invoice.line.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + quantity = fields.Float(required=False, allow_none=True) + priceUnit = fields.Float(required=False, allow_none=True) + total = fields.Float(required=False, allow_none=True) + discount = fields.Float(required=False, allow_none=True) + displayType = fields.String(required=False, allow_none=True) + saleLineId = fields.Integer(required=False, allow_none=True) + isDownPayment = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_mail.py b/pms_api_rest/datamodels/pms_mail.py new file mode 100644 index 0000000000..ba66a50ebc --- /dev/null +++ b/pms_api_rest/datamodels/pms_mail.py @@ -0,0 +1,12 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsMailInfo(Datamodel): + _name = "pms.mail.info" + mailType = fields.String(required=False, allow_none=True) + subject = fields.String(required=False, allow_none=True) + bodyMail = fields.String(required=False, allow_none=True) + partnerIds = fields.List(fields.Integer(), required=False) + emailAddresses = fields.List(fields.String(), required=False) diff --git a/pms_api_rest/datamodels/pms_notification.py b/pms_api_rest/datamodels/pms_notification.py new file mode 100644 index 0000000000..19b85e7e6a --- /dev/null +++ b/pms_api_rest/datamodels/pms_notification.py @@ -0,0 +1,20 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsNotificationSearch(Datamodel): + _name = "pms.notification.search" + fromTimestamp = fields.String(required=False) + pmsPropertyId = fields.Integer(required=False) + + +class PmsNotificationInfo(Datamodel): + _name = "pms.notification.info" + pmsPropertyId = fields.Integer(required=False) + folioId = fields.Integer(required=False) + timeStamp = fields.Integer(required=False) + folioName = fields.String(required=False) + partnerName = fields.String(required=False) + saleChannelName = fields.String(required=False, allow_none=True) + numReservationsToAssign = fields.Integer(required=False) diff --git a/pms_api_rest/datamodels/pms_ocr.py b/pms_api_rest/datamodels/pms_ocr.py new file mode 100644 index 0000000000..5e30cd7007 --- /dev/null +++ b/pms_api_rest/datamodels/pms_ocr.py @@ -0,0 +1,30 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsOcrInput(Datamodel): + _name = "pms.ocr.input" + imageBase64Front = fields.String(required=True, allow_none=False) + imageBase64Back = fields.String(required=False, allow_none=False) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + + +class PmsOcrCheckinResult(Datamodel): + _name = "pms.ocr.checkin.result" + nationality = fields.Integer(required=False, allow_none=True) + countryId = fields.Integer(required=False, allow_none=True) + firstname = fields.String(required=False, allow_none=True) + lastname = fields.String(required=False, allow_none=True) + lastname2 = fields.String(required=False, allow_none=True) + gender = fields.String(required=False, allow_none=True) + birthdate = fields.String(required=False, allow_none=True) + documentType = fields.Integer(required=False, allow_none=True) + documentExpeditionDate = fields.String(required=False, allow_none=True) + documentSupportNumber = fields.String(required=False, allow_none=True) + documentNumber = fields.String(required=False, allow_none=True) + residenceStreet = fields.String(required=False, allow_none=True) + residenceCity = fields.String(required=False, allow_none=True) + countryState = fields.Integer(required=False, allow_none=True) + documentCountryId = fields.Integer(required=False, allow_none=True) + zip = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py new file mode 100644 index 0000000000..ac38153732 --- /dev/null +++ b/pms_api_rest/datamodels/pms_partner.py @@ -0,0 +1,75 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsPartnerSearchParam(Datamodel): + _name = "pms.partner.search.param" + _inherit = "pms.rest.metadata" + id = fields.Integer(required=False, allow_none=True) + documentType = fields.Integer(required=False, allow_none=True) + documentNumber = fields.String(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + housedNow = fields.Boolean(required=False, allow_none=True) + housedLastWeek = fields.Boolean(required=False, allow_none=True) + housedLastMonth = fields.Boolean(required=False, allow_none=True) + filter = fields.String(required=False, allow_none=True) + filterByType = fields.String(required=False, allow_none=True) + + +class PmsPartnerInfo(Datamodel): + _name = "pms.partner.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + firstname = fields.String(required=False, allow_none=True) + lastname = fields.String(required=False, allow_none=True) + lastname2 = fields.String(required=False, allow_none=True) + email = fields.String(required=False, allow_none=True) + mobile = fields.String(required=False, allow_none=True) + phone = fields.String(required=False, allow_none=True) + documentType = fields.Integer(required=False, allow_none=True) + documentNumber = fields.String(required=False, allow_none=True) + documentExpeditionDate = fields.String(required=False, allow_none=True) + documentSupportNumber = fields.String(required=False, allow_none=True) + documentCountryId = fields.Integer(required=False, allow_none=True) + gender = fields.String(required=False, allow_none=True) + birthdate = fields.String(required=False, allow_none=True) + age = fields.Integer(required=False, allow_none=True) + residenceStreet = fields.String(required=False, allow_none=True) + residenceStreet2 = fields.String(required=False, allow_none=True) + residenceCity = fields.String(required=False, allow_none=True) + residenceZip = fields.String(required=False, allow_none=True) + nationality = fields.Integer(required=False, allow_none=True) + residenceStateId = fields.Integer(required=False, allow_none=True) + isAgency = fields.Boolean(required=False, allow_none=True) + isCompany = fields.Boolean(required=False, allow_none=True) + street = fields.String(required=False, allow_none=True) + street2 = fields.String(required=False, allow_none=True) + zip = fields.String(required=False, allow_none=True) + city = fields.String(required=False, allow_none=True) + stateId = fields.Integer(required=False, allow_none=True) + countryId = fields.Integer(required=False, allow_none=True) + residenceCountryId = fields.Integer(required=False, allow_none=True) + vatNumber = fields.String(required=False, allow_none=True) + vatDocumentType = fields.String(required=False, allow_none=True) + comment = fields.String(required=False, allow_none=True) + language = fields.String(required=False, allow_none=True) + userId = fields.Integer(required=False, allow_none=True) + paymentTerms = fields.Integer(required=False, allow_none=True) + pricelistId = fields.Integer(required=False, allow_none=True) + salesReference = fields.String(required=False, allow_none=True) + saleChannelId = fields.Integer(required=False, allow_none=True) + commission = fields.Integer(required=False, allow_none=True) + invoicingPolicy = fields.String(required=False, allow_none=True) + daysAutoInvoice = fields.Integer(required=False, allow_none=True) + invoicingMonthDay = fields.Integer(required=False, allow_none=True) + invoiceToAgency = fields.String(required=False, allow_none=True) + tagIds = fields.List(fields.Integer(required=False, allow_none=True)) + lastStay = fields.String(required=False, allow_none=True) + + +class PmsPartnerResults(Datamodel): + _name = "pms.partner.results" + partners = fields.List(NestedModel("pms.partner.info")) + total = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_price.py b/pms_api_rest/datamodels/pms_price.py new file mode 100644 index 0000000000..84cdb072b0 --- /dev/null +++ b/pms_api_rest/datamodels/pms_price.py @@ -0,0 +1,25 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPriceSearchParam(Datamodel): + _name = "pms.price.search.param" + dateFrom = fields.String(required=True, allow_none=True) + dateTo = fields.String(required=True, allow_none=True) + pmsPropertyId = fields.Integer(required=True, allow_none=True) + pricelistId = fields.Integer(required=True, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + boardServiceId = fields.Integer(required=False, allow_none=True) + boardServiceLineId = fields.Integer(required=False, allow_none=True) + productId = fields.Integer(required=False, allow_none=True) + productQty = fields.Integer(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + isAdults = fields.Boolean(required=False, allow_none=True) + isChildren = fields.Boolean(required=False, allow_none=True) + + +class PmsPriceInfo(Datamodel): + _name = "pms.price.info" + date = fields.String(required=True, allow_none=False) + price = fields.Float(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_pricelist.py b/pms_api_rest/datamodels/pms_pricelist.py new file mode 100644 index 0000000000..07189c658f --- /dev/null +++ b/pms_api_rest/datamodels/pms_pricelist.py @@ -0,0 +1,21 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPricelistSearch(Datamodel): + _name = "pms.pricelist.search" + pmsPropertyId = fields.Integer(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) + saleChannelId = fields.Integer(required=False, allow_none=True) + daily = fields.Boolean(required=False, allow_none=True) + + +class PmsPricelistInfo(Datamodel): + _name = "pms.pricelist.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + cancelationRuleId = fields.Integer(required=False, allow_none=True) + defaultAvailabilityPlanId = fields.Integer(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(required=False, allow_none=True)) + saleChannelIds = fields.List(fields.Integer(required=False, allow_none=True)) diff --git a/pms_api_rest/datamodels/pms_pricelist_item.py b/pms_api_rest/datamodels/pms_pricelist_item.py new file mode 100644 index 0000000000..b729dadedc --- /dev/null +++ b/pms_api_rest/datamodels/pms_pricelist_item.py @@ -0,0 +1,26 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsPricelistItemSearchParam(Datamodel): + _name = "pms.pricelist.item.search.param" + dateFrom = fields.String(required=True, allow_none=False) + dateTo = fields.String(required=True, allow_none=False) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + + +class PmsPricelistItemInfo(Datamodel): + _name = "pms.pricelist.item.info" + pricelistItemId = fields.Integer(required=False, allow_none=True) + price = fields.Float(required=False, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + pricelistId = fields.Integer(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + + +class PmsPricelistItemsInfo(Datamodel): + _name = "pms.pricelist.items.info" + pricelistItems = fields.List(NestedModel("pms.pricelist.item.info")) diff --git a/pms_api_rest/datamodels/pms_product.py b/pms_api_rest/datamodels/pms_product.py new file mode 100644 index 0000000000..9a11a0bd2d --- /dev/null +++ b/pms_api_rest/datamodels/pms_product.py @@ -0,0 +1,18 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsProductSearchParam(Datamodel): + _name = "pms.product.search.param" + name = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + + +class PmProductInfo(Datamodel): + _name = "pms.product.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + perDay = fields.Boolean(required=False, allow_none=True) + perPerson = fields.Boolean(required=False, allow_none=True) + consumedOn = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py new file mode 100644 index 0000000000..c52b736a22 --- /dev/null +++ b/pms_api_rest/datamodels/pms_property.py @@ -0,0 +1,44 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPropertySearchParam(Datamodel): + _name = "pms.property.search.param" + name = fields.String(required=False, allow_none=False) + + +class PmsPropertyInfo(Datamodel): + _name = "pms.property.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + stateName = fields.String(required=False, allow_none=True) + company = fields.String(required=False, allow_none=True) + defaultPricelistId = fields.Integer(required=False, allow_none=True) + colorOptionConfig = fields.String(required=False, allow_none=True) + preReservationColor = fields.String(required=False, allow_none=True) + confirmedReservationColor = fields.String(required=False, allow_none=True) + paidReservationColor = fields.String(required=False, allow_none=True) + onBoardReservationColor = fields.String(required=False, allow_none=True) + paidCheckinReservationColor = fields.String(required=False, allow_none=True) + outReservationColor = fields.String(required=False, allow_none=True) + staffReservationColor = fields.String(required=False, allow_none=True) + toAssignReservationColor = fields.String(required=False, allow_none=True) + overPaymentColor = fields.String(required=False, allow_none=True) + pendingPaymentReservationColor = fields.String(required=False, allow_none=True) + simpleOutColor = fields.String(required=False, allow_none=True) + simpleInColor = fields.String(required=False, allow_none=True) + simpleFutureColor = fields.String(required=False, allow_none=True) + language = fields.String(required=True, allow_none=False) + hotelImageUrl = fields.String(required=False, allow_none=True) + street = fields.String(required=False, allow_none=True) + street2 = fields.String(required=False, allow_none=True) + zip = fields.String(required=False, allow_none=True) + city = fields.String(required=False, allow_none=True) + ineCategory = fields.String(required=False, allow_none=True) + cardexWarning = fields.String(required=False, allow_none=True) + companyPrivacyPolicy = fields.String(required=False, allow_none=True) + isOCRAvailable = fields.Boolean(required=True, allow_none=False) + canDownloadIneReport = fields.Boolean(required=True, allow_none=False) + companyName = fields.String(required=False, allow_none=True) + maxAmountSimplifiedInvoice = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_report.py b/pms_api_rest/datamodels/pms_report.py new file mode 100644 index 0000000000..fc410de864 --- /dev/null +++ b/pms_api_rest/datamodels/pms_report.py @@ -0,0 +1,16 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsReportSearchParam(Datamodel): + _name = "pms.report.search.param" + dateFrom = fields.String(required=False, allow_none=True) + dateTo = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + + +class PmsTransactionReportOutput(Datamodel): + _name = "pms.report" + fileName = fields.String(required=False, allow_none=True) + binary = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py new file mode 100644 index 0000000000..6b6ab14a75 --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -0,0 +1,141 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsReservationSearchParam(Datamodel): + _name = "pms.reservation.search.param" + _inherit = "pms.rest.metadata" + pmsPropertyId = fields.Integer(required=True, allow_none=True) + toAssign = fields.Boolean(required=False, allow_none=True) + createDateFrom = fields.String(required=False, allow_none=True) + createDateTo = fields.String(required=False, allow_none=True) + ids = fields.List(fields.Integer(), required=False) + lastUpdateFrom = fields.String(required=False, allow_none=True) + + +class PmsReservationShortInfo(Datamodel): + _name = "pms.reservation.short.info" + id = fields.Integer(required=False, allow_none=True) + boardServiceId = fields.Integer(required=False, allow_none=True) + checkin = fields.String(required=False, allow_none=True) + checkout = fields.String(required=False, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + roomTypeClassId = fields.Integer(required=False, allow_none=True) + preferredRoomId = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + adults = fields.Integer(required=False, allow_none=True) + stateCode = fields.String(required=False, allow_none=True) + stateDescription = fields.String(required=False, allow_none=True) + children = fields.Integer(required=False, allow_none=True) + paymentState = fields.String(required=False, allow_none=True) + readyForCheckin = fields.Boolean(required=False, allow_none=True) + allowedCheckout = fields.Boolean(required=False, allow_none=True) + isSplitted = fields.Boolean(required=False, allow_none=True) + priceTotal = fields.Float(required=False, allow_none=True) + servicesCount = fields.Integer(required=False, allow_none=True) + folioSequence = fields.Integer(required=False, allow_none=True) + pricelistId = fields.Integer(required=False, allow_none=True) + nights = fields.Integer(required=False, allow_none=True) + numServices = fields.Integer(required=False, allow_none=True) + toAssign = fields.Boolean(required=False, allow_none=True) + overbooking = fields.Boolean(required=False, allow_none=True) + isBlocked = fields.Boolean(required=False, allow_none=True) + reservationType = fields.String(required=False, allow_none=True) + segmentationId = fields.Integer(required=False, allow_none=True) + isOverNightRoom = fields.Boolean(required=False, allow_none=True) + lastUpdateFrom = fields.String(required=False, allow_none=True) + createDate = fields.String(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + + + + +class PmsReservationInfo(Datamodel): + _name = "pms.reservation.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + folioId = fields.Integer(required=False, allow_none=True) + folioSequence = fields.Integer(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + partnerName = fields.String(required=False, allow_none=True) + boardServiceId = fields.Integer(required=False, allow_none=True) + boardServices = fields.List( + NestedModel("pms.service.info"), required=False, allow_none=True + ) + saleChannelId = fields.Integer(required=False, allow_none=True) + agencyId = fields.Integer(required=False, allow_none=True) + userId = fields.Integer(required=False, allow_none=True) + + checkin = fields.String(required=False, allow_none=True) + checkout = fields.String(required=False, allow_none=True) + arrivalHour = fields.String(required=False, allow_none=True) + departureHour = fields.String(required=False, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + preferredRoomId = fields.Integer(required=False, allow_none=True) + pricelistId = fields.Integer(required=False, allow_none=True) + + adults = fields.Integer(required=False, allow_none=True) + overbooking = fields.Boolean(required=False, allow_none=True) + externalReference = fields.String(required=False, allow_none=True) + stateCode = fields.String(required=False, allow_none=True) + stateDescription = fields.String(required=False, allow_none=True) + children = fields.Integer(required=False, allow_none=True) + readyForCheckin = fields.Boolean(required=False, allow_none=True) + checkinPartnerCount = fields.Integer(required=False, allow_none=True) + allowedCheckout = fields.Boolean(required=False, allow_none=True) + isSplitted = fields.Boolean(required=False, allow_none=True) + pendingCheckinData = fields.Integer(required=False, allow_none=True) + createDate = fields.String(required=False, allow_none=True) + segmentationId = fields.Integer(required=False, allow_none=True) + cancelationRuleId = fields.Integer(required=False, allow_none=True) + toAssign = fields.Boolean(required=False, allow_none=True) + toCheckout = fields.Boolean(required=False, allow_none=True) + undoOnboard = fields.Boolean(required=False, allow_none=True) + reservationType = fields.String(required=False, allow_none=True) + + priceTotal = fields.Float(required=False, allow_none=True) + priceTax = fields.Float(required=False, allow_none=True) + discount = fields.Float(required=False, allow_none=True) + servicesDiscount = fields.Float(required=False, allow_none=True) + commissionAmount = fields.Float(required=False, allow_none=True) + commissionPercent = fields.Float(required=False, allow_none=True) + priceOnlyServices = fields.Float(required=False, allow_none=True) + priceOnlyRoom = fields.Float(required=False, allow_none=True) + nights = fields.Integer(required=False, allow_none=True) + numServices = fields.Integer(required=False, allow_none=True) + + reservationLines = fields.List(NestedModel("pms.reservation.line.info")) + services = fields.List( + NestedModel("pms.service.info"), required=False, allow_none=True + ) + partnerRequests = fields.String(required=False, allow_none=True) + isReselling = fields.Boolean(required=False, allow_none=True) + createdBy = fields.String(required=False, allow_none=True) + isBlocked = fields.Boolean(required=False, allow_none=True) + + partnerId = fields.Integer(required=False, allow_none=True) + partnerEmail = fields.String(required=False, allow_none=True) + partnerPhone = fields.String(required=False, allow_none=True) + + # TODO: Refact + # messages = fields.List(fields.Dict(required=False, allow_none=True)) + + +class PmsReservationPublicInfo(Datamodel): + _name = "pms.reservation.public.info" + id = fields.Integer(required=False, allow_none=True) + roomTypeName = fields.String(required=False, allow_none=True) + reservationReference = fields.String(required=False, allow_none=True) + checkinNamesCompleted = fields.List(fields.String(required=False, allow_none=True)) + accessToken = fields.String(required=False, allow_none=True) + nights = fields.Integer(required=False, allow_none=True) + checkin = fields.String(required=False, allow_none=True) + checkout = fields.String(required=False, allow_none=True) + adults = fields.Integer(required=False, allow_none=True) + children = fields.Integer(required=False, allow_none=True) + reservationAmount = fields.Float(required=False, allow_none=True) + checkinPartners = fields.List( + NestedModel("pms.checkin.partner.info"), required=False, allow_none=False + ) diff --git a/pms_api_rest/datamodels/pms_reservation_line.py b/pms_api_rest/datamodels/pms_reservation_line.py new file mode 100644 index 0000000000..6d76b95ed6 --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation_line.py @@ -0,0 +1,30 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsReservationLineSearchParam(Datamodel): + _name = "pms.reservation.line.search.param" + date = fields.String(required=False, allow_none=False) + dateFrom = fields.String(required=False, allow_none=False) + dateTo = fields.String(required=False, allow_none=False) + reservationId = fields.Integer(required=False, allow_none=False) + pmsPropertyId = fields.Integer(required=False, allow_none=False) + roomId = fields.Integer(required=False, allow_none=False) + overbooking = fields.Boolean(required=False, allow_none=False) + + +class PmsReservationLineInfo(Datamodel): + _name = "pms.reservation.line.info" + id = fields.Integer(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + price = fields.Float(required=False, allow_none=True) + discount = fields.Float(required=False, allow_none=True) + cancelDiscount = fields.Float(required=False, allow_none=True) + roomId = fields.Integer(required=False, allow_none=True) + reservationId = fields.Integer(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + isReselling = fields.Boolean(required=False, allow_none=True) + reservationType = fields.String(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + isSplitted = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_reservation_message.py b/pms_api_rest/datamodels/pms_reservation_message.py new file mode 100644 index 0000000000..3b004d7ed0 --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation_message.py @@ -0,0 +1,37 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsReservationMessageInfo(Datamodel): + _name = "pms.reservation.message.info" + reservationId = fields.Integer(required=False, allow_none=True) + author = fields.String(required=False, allow_none=True) + message = fields.String(required=False, allow_none=True) + subject = fields.String(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + messageType = fields.String(required=False, allow_none=True) + authorImageBase64 = fields.String(required=False, allow_none=True) + authorImageUrl = fields.String(required=False, allow_none=True) + + +class PmsFolioMessageInfo(Datamodel): + _name = "pms.folio.message.info" + author = fields.String(required=False, allow_none=True) + message = fields.String(required=False, allow_none=True) + subject = fields.String(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + messageType = fields.String(required=False, allow_none=True) + authorImageBase64 = fields.String(required=False, allow_none=True) + authorImageUrl = fields.String(required=False, allow_none=True) + + +class PmsMessageInfo(Datamodel): + _name = "pms.message.info" + folioMessages = fields.List( + NestedModel("pms.reservation.message.info"), required=False, allow_none=True + ) + reservationMessages = fields.List( + NestedModel("pms.reservation.message.info"), required=False, allow_none=True + ) diff --git a/pms_api_rest/datamodels/pms_rest_metadata.py b/pms_api_rest/datamodels/pms_rest_metadata.py new file mode 100644 index 0000000000..1ca229c04c --- /dev/null +++ b/pms_api_rest/datamodels/pms_rest_metadata.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsRestMetadata(Datamodel): + _name = "pms.rest.metadata" + orderBy = fields.String(required=False, allow_none=True) + limit = fields.Integer(required=False, allow_none=True) + offset = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py new file mode 100644 index 0000000000..55a4e2ebcb --- /dev/null +++ b/pms_api_rest/datamodels/pms_room.py @@ -0,0 +1,27 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsRoomSearchParam(Datamodel): + _name = "pms.room.search.param" + name = fields.String(required=False, allow_none=False) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + availabilityFrom = fields.String(required=False, allow_none=False) + availabilityTo = fields.String(required=False, allow_none=False) + currentLines = fields.List(fields.Integer(), required=False, allow_none=False) + pricelistId = fields.Integer(required=False, allow_none=False) + + +class PmsRoomInfo(Datamodel): + _name = "pms.room.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + capacity = fields.Integer(required=False, allow_none=True) + shortName = fields.String(required=False, allow_none=True) + roomTypeClassId = fields.Integer(required=False, allow_none=True) + ubicationId = fields.Integer(required=False, allow_none=True) + extraBedsAllowed = fields.Integer(required=False, allow_none=True) + roomAmenityIds = fields.List(fields.Integer(), required=False, allow_none=True) + roomAmenityInName = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room_closure_reason.py b/pms_api_rest/datamodels/pms_room_closure_reason.py new file mode 100644 index 0000000000..410d8b3e04 --- /dev/null +++ b/pms_api_rest/datamodels/pms_room_closure_reason.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsRoomClosureReasonInfo(Datamodel): + _name = "pms.room.closure.reason.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + description = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room_type.py b/pms_api_rest/datamodels/pms_room_type.py new file mode 100644 index 0000000000..301672e7b2 --- /dev/null +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -0,0 +1,22 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsRoomTypeSearchParam(Datamodel): + _name = "pms.room.type.search.param" + name = fields.String(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) + + +class PmsRoomTypeInfo(Datamodel): + _name = "pms.room.type.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) + defaultCode = fields.String(required=False, allow_none=True) + classId = fields.Integer(required=False, allow_none=True) + price = fields.Float(required=False, allow_none=True) + minPrice = fields.Float(required=False, allow_none=True) + defaultMaxAvail = fields.Integer(required=False, allow_none=True) + defaultQuota = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room_type_class.py b/pms_api_rest/datamodels/pms_room_type_class.py new file mode 100644 index 0000000000..b10df266fe --- /dev/null +++ b/pms_api_rest/datamodels/pms_room_type_class.py @@ -0,0 +1,18 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsRoomTypeClassSearchParam(Datamodel): + _name = "pms.room.type.class.search.param" + name = fields.String(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) + + +class PmsRoomTypeClassInfo(Datamodel): + _name = "pms.room.type.class.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + defaultCode = fields.String(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) + imageUrl = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_sale_channel.py b/pms_api_rest/datamodels/pms_sale_channel.py new file mode 100644 index 0000000000..6b5cc99e33 --- /dev/null +++ b/pms_api_rest/datamodels/pms_sale_channel.py @@ -0,0 +1,18 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsSaleChannelSearchParam(Datamodel): + _name = "pms.sale.channel.search.param" + pmsPropertyIds = fields.List(fields.Integer(), required=False) + isOnLine = fields.Boolean(required=False, allow_none=True) + + +class PmsSaleChannelInfo(Datamodel): + _name = "pms.sale.channel.info" + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) + channelType = fields.String(required=True, allow_none=True) + iconUrl = fields.String(required=False, allow_none=True) + isOnLine = fields.Boolean(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_search_param.py b/pms_api_rest/datamodels/pms_search_param.py new file mode 100644 index 0000000000..83ffe60c84 --- /dev/null +++ b/pms_api_rest/datamodels/pms_search_param.py @@ -0,0 +1,11 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsSearchParam(Datamodel): + _name = "pms.search.param" + + pmsPropertyId = fields.Integer(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) + ids = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py new file mode 100644 index 0000000000..09dea73acd --- /dev/null +++ b/pms_api_rest/datamodels/pms_service.py @@ -0,0 +1,22 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsServiceInfo(Datamodel): + _name = "pms.service.info" + id = fields.Integer(required=False, allow_none=True) + reservationId = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + productId = fields.Integer(required=False, allow_none=True) + quantity = fields.Integer(required=False, allow_none=True) + priceTotal = fields.Float(required=False, allow_none=True) + priceSubtotal = fields.Float(required=False, allow_none=True) + priceTaxes = fields.Float(required=False, allow_none=True) + discount = fields.Float(required=False, allow_none=True) + isBoardService = fields.Boolean(required=False, allow_none=True) + serviceLines = fields.List(NestedModel("pms.service.line.info")) + priceUnit = fields.Float(required=False, allow_none=True) + isCancelPenalty = fields.Boolean(required=False, allow_none=True) + boardServiceLineId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_service_line.py b/pms_api_rest/datamodels/pms_service_line.py new file mode 100644 index 0000000000..b1d2425e10 --- /dev/null +++ b/pms_api_rest/datamodels/pms_service_line.py @@ -0,0 +1,12 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsServiceLineInfo(Datamodel): + _name = "pms.service.line.info" + id = fields.Integer(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + priceUnit = fields.Float(required=False, allow_none=True) + discount = fields.Float(required=False, allow_none=True) + quantity = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_transaction.py b/pms_api_rest/datamodels/pms_transaction.py new file mode 100644 index 0000000000..ee5f356a0f --- /dev/null +++ b/pms_api_rest/datamodels/pms_transaction.py @@ -0,0 +1,43 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsTransactionSearchParam(Datamodel): + _name = "pms.transaction.search.param" + _inherit = "pms.rest.metadata" + pmsPropertyId = fields.Integer(required=True, allow_none=False) + filter = fields.String(required=False, allow_none=True) + dateStart = fields.String(required=False, allow_none=True) + dateEnd = fields.String(required=False, allow_none=True) + transactionMethodId = fields.Integer(required=False, allow_none=True) + transactionType = fields.String(required=False, allow_none=True) + + +class PmsTransactionsResults(Datamodel): + _name = "pms.transaction.results" + transactions = fields.List(NestedModel("pms.transaction.info")) + total = fields.Float(required=False, allow_none=True) + totalTransactions = fields.Integer(required=False, allow_none=True) + + +class PmsTransactionInfo(Datamodel): + _name = "pms.transaction.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + journalId = fields.Integer(required=False, allow_none=True) + amount = fields.Float(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + reservationIds = fields.List(fields.Integer(), required=False) + folioId = fields.Integer(required=False, allow_none=True) + destinationJournalId = fields.Integer(required=False, allow_none=True) + reference = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + createUid = fields.Integer(required=False, allow_none=True) + transactionType = fields.String(required=False, allow_none=True) + isReconcilied = fields.Boolean(required=False, allow_none=True) + downPaymentInvoiceId = fields.Integer(required=False, allow_none=True) + # REVIEW: Fields to avoid?: + partnerName = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_ubication.py b/pms_api_rest/datamodels/pms_ubication.py new file mode 100644 index 0000000000..03f1c51582 --- /dev/null +++ b/pms_api_rest/datamodels/pms_ubication.py @@ -0,0 +1,16 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsUbicationSearchParam(Datamodel): + _name = "pms.ubication.search.param" + name = fields.String(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) + + +class PmsUbicationInfo(Datamodel): + _name = "pms.ubication.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py new file mode 100644 index 0000000000..e82789b43f --- /dev/null +++ b/pms_api_rest/datamodels/pms_user.py @@ -0,0 +1,38 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsApiRestUserInput(Datamodel): + _name = "pms.api.rest.user.input" + username = fields.String(required=False, allow_none=True) + password = fields.String(required=False, allow_none=True) + newPassword = fields.String(required=False, allow_none=True) + userId = fields.Integer(required=False, allow_none=True) + userEmail = fields.String(required=False, allow_none=True) + resetToken = fields.String(required=False, allow_none=True) + url = fields.String(required=False, allow_none=True) + + +class PmsApiRestUserOutput(Datamodel): + _name = "pms.api.rest.user.output" + token = fields.String(required=False, allow_none=True) + expirationDate = fields.Integer(required=False, allow_none=True) + userId = fields.Integer(required=True, allow_none=False) + userName = fields.String(required=True, allow_none=False) + userFirstName = fields.String(required=False, allow_none=True) + userEmail = fields.String(required=False, allow_none=True) + userPhone = fields.String(required=False, allow_none=True) + userImageBase64 = fields.String(required=False, allow_none=True) + userImageUrl = fields.String(required=False, allow_none=True) + defaultPropertyId = fields.Integer(required=False, allow_none=True) + defaultPropertyName = fields.String(required=False, allow_none=True) + isNewInterfaceUser = fields.Boolean(required=False, allow_none=True) + availabilityRuleFields = fields.List( + fields.String(), required=False, allow_none=True + ) + + +class PmsApiRestUserLoginOutput(Datamodel): + _name = "pms.api.rest.user.login.output" + login = fields.String(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_wizard_state.py b/pms_api_rest/datamodels/pms_wizard_state.py new file mode 100644 index 0000000000..d6b6afd263 --- /dev/null +++ b/pms_api_rest/datamodels/pms_wizard_state.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsWizardStateInfo(Datamodel): + _name = "pms.wizard.state.info" + code = fields.String(required=True, allow_none=False) + title = fields.String(required=False, allow_none=True) + text = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/res_city_zip.py b/pms_api_rest/datamodels/res_city_zip.py new file mode 100644 index 0000000000..cc19513a1a --- /dev/null +++ b/pms_api_rest/datamodels/res_city_zip.py @@ -0,0 +1,18 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class ResCityZipSearchParam(Datamodel): + _name = "res.city.zip.search.param" + address = fields.String(required=False, allow_none=False) + + +class ResCityZipInfo(Datamodel): + _name = "res.city.zip.info" + resZipId = fields.Integer(required=False, allow_none=True) + cityId = fields.String(required=False, allow_none=True) + stateId = fields.Integer(required=False, allow_none=True) + stateName = fields.String(required=False, allow_none=True) + countryId = fields.Integer(required=False, allow_none=True) + zipCode = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/res_country.py b/pms_api_rest/datamodels/res_country.py new file mode 100644 index 0000000000..f8b8fdbf14 --- /dev/null +++ b/pms_api_rest/datamodels/res_country.py @@ -0,0 +1,16 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsResCountriesInfo(Datamodel): + _name = "res.country.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + code = fields.String(required=False, allow_none=True) + + +class PmsResCountryStatesInfo(Datamodel): + _name = "res.country_state.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/res_lang.py b/pms_api_rest/datamodels/res_lang.py new file mode 100644 index 0000000000..1a0c250233 --- /dev/null +++ b/pms_api_rest/datamodels/res_lang.py @@ -0,0 +1,9 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsResLangInfo(Datamodel): + _name = "res.lang.info" + code = fields.String(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/res_partner_category.py b/pms_api_rest/datamodels/res_partner_category.py new file mode 100644 index 0000000000..c41b39e4ba --- /dev/null +++ b/pms_api_rest/datamodels/res_partner_category.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class ResPartnerCategoryInfo(Datamodel): + _name = "res.partner.category.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + parentId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/res_users.py b/pms_api_rest/datamodels/res_users.py new file mode 100644 index 0000000000..bd4bb4da1b --- /dev/null +++ b/pms_api_rest/datamodels/res_users.py @@ -0,0 +1,11 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsResUsersInfo(Datamodel): + _name = "res.users.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + userFirstName = fields.String(required=False, allow_none=True) + userImageBase64 = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/demo/pms_api_rest_master_data.xml b/pms_api_rest/demo/pms_api_rest_master_data.xml new file mode 100644 index 0000000000..f0bee25861 --- /dev/null +++ b/pms_api_rest/demo/pms_api_rest_master_data.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pms_api_rest/demo/pms_property_hotel_image_pms_api_rest_my_property.jpg b/pms_api_rest/demo/pms_property_hotel_image_pms_api_rest_my_property.jpg new file mode 100644 index 0000000000..9224272068 Binary files /dev/null and b/pms_api_rest/demo/pms_property_hotel_image_pms_api_rest_my_property.jpg differ diff --git a/pms_api_rest/demo/pms_property_hotel_image_pms_api_rest_san_carlos.jpg b/pms_api_rest/demo/pms_property_hotel_image_pms_api_rest_san_carlos.jpg new file mode 100644 index 0000000000..652f92a04f Binary files /dev/null and b/pms_api_rest/demo/pms_property_hotel_image_pms_api_rest_san_carlos.jpg differ diff --git a/pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_conference.svg b/pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_conference.svg new file mode 100644 index 0000000000..f61891c680 --- /dev/null +++ b/pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_conference.svg @@ -0,0 +1,38 @@ + + + +Created with Fabric.js 5.2.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_parking.svg b/pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_parking.svg new file mode 100644 index 0000000000..4db7ce0fbb --- /dev/null +++ b/pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_parking.svg @@ -0,0 +1,26 @@ + + + +Created with Fabric.js 5.2.4 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_room.svg b/pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_room.svg new file mode 100644 index 0000000000..bf283faf6c --- /dev/null +++ b/pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_room.svg @@ -0,0 +1,3 @@ + + + diff --git a/pms_api_rest/http.py b/pms_api_rest/http.py new file mode 100644 index 0000000000..e7b165367f --- /dev/null +++ b/pms_api_rest/http.py @@ -0,0 +1,89 @@ +from werkzeug.exceptions import ( + BadRequest, + Forbidden, + HTTPException, + InternalServerError, + NotFound, + Unauthorized, +) + +import odoo +from odoo.exceptions import ( + AccessDenied, + AccessError, + MissingError, + UserError, + ValidationError, +) +from odoo.http import HttpRequest, Root, SessionExpiredException +from odoo.loglevels import ustr + +from odoo.addons.base_rest.http import ( + HttpRestRequest, + _rest_services_routes, + wrapJsonException, +) + + +class HttpRestRequestPms(HttpRestRequest): + def __init__(self, httprequest): + super(HttpRestRequestPms, self).__init__(httprequest) + + def _handle_exception(self, exception): + """Called within an except block to allow converting exceptions + to abitrary responses. Anything returned (except None) will + be used as response.""" + if isinstance(exception, SessionExpiredException): + # we don't want to return the login form as plain html page + # we want to raise a proper exception + return wrapJsonException(Unauthorized(ustr(exception))) + try: + return super(HttpRequest, self)._handle_exception(exception) + except MissingError as e: + extra_info = getattr(e, "rest_json_info", None) + return wrapJsonException( + NotFound(ustr(e)), include_description=True, extra_info=extra_info + ) + except (AccessError, AccessDenied) as e: + extra_info = getattr(e, "rest_json_info", None) + return wrapJsonException( + Forbidden(ustr(e)), include_description=True, extra_info=extra_info + ) + except (UserError, ValidationError, ValueError) as e: + extra_info = getattr(e, "rest_json_info", None) + return wrapJsonException( + BadRequest(e.args[0]), include_description=True, extra_info=extra_info + ) + except HTTPException as e: + extra_info = getattr(e, "rest_json_info", None) + return wrapJsonException(e, include_description=True, extra_info=extra_info) + except Unauthorized as e: + extra_info = getattr(e, "rest_json_info", None) + return ( + wrapJsonException(e, include_description=True, extra_info=extra_info), + ) + + except Exception as e: # flake8: noqa: E722 + extra_info = getattr(e, "rest_json_info", None) + return wrapJsonException(InternalServerError(e), extra_info=extra_info) + + +ori_get_request = Root.get_request + + +def get_request(self, httprequest): + db = httprequest.session.db + if db and odoo.service.db.exp_db_exist(db): + # on the very first request processed by a worker, + # registry is not loaded yet + # so we enforce its loading here to make sure that + # _rest_services_databases is not empty + odoo.registry(db) + rest_routes = _rest_services_routes.get(db, []) + for root_path in rest_routes: + if httprequest.path.startswith(root_path): + return HttpRestRequestPms(httprequest) + return ori_get_request(self, httprequest) + + +Root.get_request = get_request diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py new file mode 100644 index 0000000000..a28070577d --- /dev/null +++ b/pms_api_rest/models/__init__.py @@ -0,0 +1,12 @@ +from . import pms_property +from . import res_users +from . import account_payment +from . import sql_export +from . import pms_room_type_class +from . import account_bank_statement +from . import product_template +from . import ota_property_settings +from . import pms_api_log +from . import pms_folio +from . import pms_checkin_partner +from . import pms_reservation diff --git a/pms_api_rest/models/account_bank_statement.py b/pms_api_rest/models/account_bank_statement.py new file mode 100644 index 0000000000..ed9cc3b84f --- /dev/null +++ b/pms_api_rest/models/account_bank_statement.py @@ -0,0 +1,43 @@ +from odoo import api, fields, models + + +class AccountBankStatement(models.Model): + _inherit = "account.bank.statement" + _order = "date desc, cash_turn desc, name desc, id desc" + + cash_turn = fields.Integer( + string="Turn", + help="Set the day turn of the cash statement", + copy=False, + readonly=True, + compute="_compute_cash_turn", + store=True, + ) + + @api.depends("journal_id", "pms_property_id", "date") + def _compute_cash_turn(self): + for record in self: + if record.journal_id.type == "cash" and record.pms_property_id: + day_statements = self.search( + [ + ("journal_id.type", "=", "cash"), + ("pms_property_id", "=", record.pms_property_id.id), + ("date", "=", record.date), + ], + order="create_date asc", + ) + record.cash_turn = list(day_statements).index(record) + 1 + + def name_get(self): + result = [] + for record in self: + name = record.name + if record.cash_turn: + name += ( + " [%s]" % str(record.cash_turn) + + " (" + + record.create_uid.name + + ")" + ) + result.append((record.id, name)) + return result diff --git a/pms_api_rest/models/account_payment.py b/pms_api_rest/models/account_payment.py new file mode 100644 index 0000000000..364a33bcfb --- /dev/null +++ b/pms_api_rest/models/account_payment.py @@ -0,0 +1,41 @@ +from odoo import api, fields, models + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + pms_api_transaction_type = fields.Selection( + selection=[ + ("customer_inbound", "Customer Payment"), + ("customer_outbound", "Customer Refund"), + ("supplier_outbound", "Supplier Payment"), + ("supplier_inbound", "Supplier Refund"), + ("internal_transfer", "Internal Transfer"), + ], + string="PMS API Transaction Type", + help="Transaction type for PMS API", + compute="_compute_pms_api_transaction_type", + ) + pms_api_counterpart_payment_id = fields.Many2one( + comodel_name="account.payment", + string="Int. Transfer Counterpart", + help="Payment counterpart for internal transfer", + ) + + @api.depends("payment_type", "partner_type") + def _compute_pms_api_transaction_type(self): + for record in self: + if record.is_internal_transfer: + record.pms_api_transaction_type = "internal_transfer" + elif record.partner_type == "customer": + if record.payment_type == "inbound": + record.pms_api_transaction_type = "customer_inbound" + else: + record.pms_api_transaction_type = "customer_outbound" + elif record.partner_type == "supplier": + if record.payment_type == "outbound": + record.pms_api_transaction_type = "supplier_outbound" + else: + record.pms_api_transaction_type = "supplier_inbound" + else: + record.pms_api_transaction_type = False diff --git a/pms_api_rest/models/ota_property_settings.py b/pms_api_rest/models/ota_property_settings.py new file mode 100644 index 0000000000..1e79642bd8 --- /dev/null +++ b/pms_api_rest/models/ota_property_settings.py @@ -0,0 +1,49 @@ +from odoo import fields, models + + +class OtaPropertySettings(models.Model): + _name = "ota.property.settings" + + pms_property_id = fields.Many2one( + string="PMS Property", + help="PMS Property", + comodel_name="pms.property", + default=lambda self: self.env.user.get_active_property_ids()[0], + ) + agency_id = fields.Many2one( + string="Partner", + help="Partner", + comodel_name="res.partner", + domain=[("is_agency", "=", True)], + ) + pms_api_alowed_payments = fields.Boolean( + string="PMS API Allowed Payments", + help="PMS API Allowed Payments", + ) + pms_api_payment_journal_id = fields.Many2one( + string="Payment Journal", + help="Payment Journal", + comodel_name="account.journal", + ) + pms_api_payment_identifier = fields.Char( + string="Payment Identifier", + help=""" + Text string used by the OTA to identify a prepaid reservation. + The string will be searched within the partnerRequests parameter. + """, + ) + main_avail_plan_id = fields.Many2one( + string="Main Availability Plan", + help="Main Availability Plan", + comodel_name="pms.availability.plan", + ) + main_pricelist_id = fields.Many2one( + string="Main Pricelist", + help="Main Pricelist", + comodel_name="product.pricelist", + ) + excluded_room_type_ids = fields.Many2many( + string="Excluded Room Types", + help="Excluded Room Types", + comodel_name="pms.room.type", + ) diff --git a/pms_api_rest/models/pms_api_log.py b/pms_api_rest/models/pms_api_log.py new file mode 100644 index 0000000000..0b0a9f7ae8 --- /dev/null +++ b/pms_api_rest/models/pms_api_log.py @@ -0,0 +1,156 @@ +from datetime import timedelta + +from odoo import _, fields, models + + +class PmsApiLog(models.Model): + _name = "pms.api.log" + + pms_property_id = fields.Many2one( + string="PMS Property", + help="PMS Property", + comodel_name="pms.property", + default=lambda self: self.env.user.get_active_property_ids()[0], + ) + client_id = fields.Many2one( + string="Client", + help="API Client", + comodel_name="res.users", + ) + request = fields.Text( + string="Request", + help="Request", + ) + response = fields.Text( + string="Response", + help="Response", + ) + status = fields.Selection( + string="Status", + help="Status", + selection=[("success", "Success"), ("error", "Error")], + ) + request_date = fields.Datetime( + string="Request Date", + help="Request Date", + ) + response_date = fields.Datetime( + string="Response Date", + help="Response Date", + ) + request_duration = fields.Float( + string="Request Duration", + help="Request Duration", + ) + method = fields.Char( + string="Method", + help="Method", + ) + endpoint = fields.Char( + string="Endpoint", + help="Endpoint", + ) + request_size = fields.Integer( + string="Request Size", + help="Request Size", + ) + response_size = fields.Integer( + string="Response Size", + help="Response Size", + ) + request_headers = fields.Text( + string="Request Headers", + help="Request Headers", + ) + response_headers = fields.Text( + string="Response Headers", + help="Response Headers", + ) + request_url = fields.Char( + string="Request URL", + help="Request URL", + ) + response_url = fields.Char( + string="Response URL", + help="Response URL", + ) + 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): + """Open a form view with the record(s) of the record log. + + For instance, for a job on a ``pms.folio``, it will open a + ``pms.product`` form view with the product record(s) concerned by + the job. If the job concerns more than one record, it opens them in a + list. + + This is the default related action. + + """ + self.ensure_one() + records = self.folio_ids + if not records: + return None + action = { + "name": _("Related Record"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": records._name, + } + if len(records) == 1: + action["res_id"] = records.id + else: + action.update( + { + "name": _("Related Records"), + "view_mode": "tree,form", + "domain": [("id", "in", records.ids)], + } + ) + return action + + 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. + + """ + self.sudo().search( + [ + ("status", "=", "success"), + ("create_date", "<", fields.Datetime.now() - timedelta(days=offset)), + ] + ).unlink() diff --git a/pms_api_rest/models/pms_checkin_partner.py b/pms_api_rest/models/pms_checkin_partner.py new file mode 100644 index 0000000000..8aeb909be6 --- /dev/null +++ b/pms_api_rest/models/pms_checkin_partner.py @@ -0,0 +1,18 @@ +from odoo import fields, models + + +class PmsCheckinPartner(models.Model): + _inherit = "pms.checkin.partner" + + origin_input_data = fields.Selection( + [ + ("wizard", "Wizard"), + ("wizard-precheckin", "Wizard-Precheckin"), + ("form", "Form"), + ("regular_customer", "Regular Customer"), + ("ocr", "OCR"), + ("ocr-precheckin", "OCR-Precheckin"), + ("precheckin", "Precheckin"), + ], + string="Origin Input Data", + ) diff --git a/pms_api_rest/models/pms_folio.py b/pms_api_rest/models/pms_folio.py new file mode 100644 index 0000000000..6e87063e13 --- /dev/null +++ b/pms_api_rest/models/pms_folio.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class PmsFolio(models.Model): + _inherit = "pms.folio" + api_rest_id = fields.Char(string="API Rest ID", help="API Rest ID") + + pms_api_log_ids = fields.Many2many( + string="API Logs", + help="API Logs", + comodel_name="pms.api.log", + 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 new file mode 100644 index 0000000000..55f7d5adf1 --- /dev/null +++ b/pms_api_rest/models/pms_property.py @@ -0,0 +1,713 @@ +import datetime +import json +import logging + +import requests + +from odoo import api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + + +class PmsProperty(models.Model): + _inherit = "pms.property" + + color_option_config = fields.Selection( + string="Color Option Configuration", + help="Configuration of the color code for the planning.", + selection=[("simple", "Simple"), ("advanced", "Advanced")], + default="simple", + ) + + simple_out_color = fields.Char( + string="Reservations Outside", + help="Color for done reservations in the planning.", + default="rgba(94,208,236)", + ) + + simple_in_color = fields.Char( + string="Reservations Inside", + help="Color for onboard and departure_delayed reservations in the planning.", + default="rgba(0,146,183)", + ) + + simple_future_color = fields.Char( + string="Future Reservations", + help="Color for confirm, arrival_delayed and draft reservations in the planning.", + default="rgba(1,182,227)", + ) + + pre_reservation_color = fields.Char( + string="Pre-Reservation", + help="Color for draft reservations in the planning.", + default="rgba(162,70,128)", + ) + + confirmed_reservation_color = fields.Char( + string="Confirmed Reservation", + default="rgba(1,182,227)", + help="Color for confirm reservations in the planning.", + ) + + paid_reservation_color = fields.Char( + string="Paid Reservation", + help="Color for done paid reservations in the planning.", + default="rgba(126,126,126)", + ) + + on_board_reservation_color = fields.Char( + string="Checkin", + help="Color for onboard not paid reservations in the planning.", + default="rgba(255,64,64)", + ) + + paid_checkin_reservation_color = fields.Char( + string="Paid Checkin", + help="Color for onboard paid reservations in the planning.", + default="rgba(130,191,7)", + ) + + out_reservation_color = fields.Char( + string="Checkout", + help="Color for done not paid reservations in the planning.", + default="rgba(88,77,118)", + ) + + staff_reservation_color = fields.Char( + string="Staff", + help="Color for staff reservations in the planning.", + default="rgba(192,134,134)", + ) + + to_assign_reservation_color = fields.Char( + string="OTA Reservation To Assign", + help="Color for to_assign reservations in the planning.", + default="rgba(237,114,46)", + ) + + pending_payment_reservation_color = fields.Char( + string="Payment Pending", + help="Color for pending payment reservations in the planning.", + default="rgba(162,70,137)", + ) + + overpayment_reservation_color = fields.Char( + string="Overpayment", + help="Color for pending payment reservations in the planning.", + default="rgba(4, 95, 118)", + ) + + hotel_image_pms_api_rest = fields.Image( + string="Hotel image", + store=True, + ) + + ota_property_settings_ids = fields.One2many( + string="OTA Property Settings", + help="OTA Property Settings", + comodel_name="ota.property.settings", + inverse_name="pms_property_id", + ) + + ocr_checkin_supplier = fields.Selection( + string="OCR Checkin Supplier", + help="Select ocr supplier for checkin documents", + selection=[], + ) + + # PUSH API NOTIFICATIONS + def get_payload_avail(self, avails, client): + self.ensure_one() + endpoint = client.url_endpoint_availability + pms_property_id = self.id + avails_dict = {"pmsPropertyId": pms_property_id, "avails": []} + room_type_ids = avails.mapped("room_type_id.id") + property_client_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", self.id), + ("agency_id", "=", client.partner_id.id), + ] + ) + plan_avail = property_client_conf.main_avail_plan_id + for room_type_id in room_type_ids: + room_type_avails = sorted( + avails.filtered(lambda r: r.room_type_id.id == room_type_id), + key=lambda r: r.date, + ) + avail_room_type_index = {} + for record_avail in room_type_avails: + avail_rule = record_avail.avail_rule_ids.filtered( + lambda r: r.availability_plan_id == plan_avail + ) + if avail_rule: + avail = avail_rule.plan_avail + else: + room_type = avail_rule.room_type_id + avail = min( + [ + record_avail.real_avail, + room_type.default_max_avail + if room_type.default_max_avail >= 0 + else record_avail.real_avail, + room_type.default_quota + if room_type.default_quota >= 0 + else record_avail.real_avail, + ] + ) + previus_date = record_avail.date - datetime.timedelta(days=1) + avail_index = avail_room_type_index.get(previus_date) + if avail_index and avail_index["avail"] == avail: + avail_room_type_index[record_avail.date] = { + "date_from": avail_index["date_from"], + "date_to": datetime.datetime.strftime( + record_avail.date, "%Y-%m-%d" + ), + "roomTypeId": room_type_id, + "avail": avail, + } + avail_room_type_index.pop(previus_date) + else: + avail_room_type_index[record_avail.date] = { + "date_from": datetime.datetime.strftime( + record_avail.date, "%Y-%m-%d" + ), + "date_to": datetime.datetime.strftime( + record_avail.date, "%Y-%m-%d" + ), + "roomTypeId": room_type_id, + "avail": avail, + } + avails_dict["avails"].extend(avail_room_type_index.values()) + return avails_dict, endpoint + + def get_payload_prices(self, prices, client): + self.ensure_one() + endpoint = client.url_endpoint_prices + pms_property_id = self.id + prices_dict = {"pmsPropertyId": pms_property_id, "prices": []} + product_ids = prices.mapped("product_id.id") + for product_id in product_ids: + room_type_id = ( + self.env["pms.room.type"].search([("product_id", "=", product_id)]).id + ) + product_prices = sorted( + prices.filtered(lambda r: r.product_id.id == product_id), + key=lambda r: r.date_end_consumption, + ) + price_product_index = {} + for price in product_prices: + previus_date = price.date_end_consumption - datetime.timedelta(days=1) + price_index = price_product_index.get(previus_date) + if price_index and round(price_index["price"], 2) == round( + price.fixed_price, 2 + ): + price_product_index[price.date_end_consumption] = { + "date_from": price_index["date_from"], + "date_to": datetime.datetime.strftime( + price.date_end_consumption, "%Y-%m-%d" + ), + "roomTypeId": room_type_id, + "price": price.fixed_price, + } + price_product_index.pop(previus_date) + else: + price_product_index[price.date_end_consumption] = { + "date_from": datetime.datetime.strftime( + price.date_end_consumption, "%Y-%m-%d" + ), + "date_to": datetime.datetime.strftime( + price.date_end_consumption, "%Y-%m-%d" + ), + "roomTypeId": room_type_id, + "price": price.fixed_price, + } + prices_dict["prices"].extend(price_product_index.values()) + return prices_dict, endpoint + + def get_payload_rules(self, rules, client): + self.ensure_one() + endpoint = client.url_endpoint_rules + pms_property_id = self.id + rules_dict = {"pmsPropertyId": pms_property_id, "rules": []} + room_type_ids = rules.mapped("room_type_id.id") + for room_type_id in room_type_ids: + room_type_rules = sorted( + rules.filtered(lambda r: r.room_type_id.id == room_type_id), + key=lambda r: r.date, + ) + rules_room_type_index = {} + for rule in room_type_rules: + previus_date = rule.date - datetime.timedelta(days=1) + avail_index = rules_room_type_index.get(previus_date) + if ( + avail_index + and avail_index["min_stay"] == rule.min_stay + and avail_index["max_stay"] == rule.max_stay + and avail_index["closed"] == rule.closed + and avail_index["closed_arrival"] == rule.closed_arrival + and avail_index["closed_departure"] == rule.closed_departure + ): + rules_room_type_index[rule.date] = { + "date_from": avail_index["date_from"], + "date_to": datetime.datetime.strftime(rule.date, "%Y-%m-%d"), + "roomTypeId": room_type_id, + "min_stay": rule.min_stay, + "max_stay": rule.max_stay, + "closed": rule.closed, + "closed_arrival": rule.closed_arrival, + "closed_departure": rule.closed_departure, + } + rules_room_type_index.pop(previus_date) + else: + rules_room_type_index[rule.date] = { + "date_from": datetime.datetime.strftime(rule.date, "%Y-%m-%d"), + "date_to": datetime.datetime.strftime(rule.date, "%Y-%m-%d"), + "roomTypeId": room_type_id, + "min_stay": rule.min_stay, + "max_stay": rule.max_stay, + "closed": rule.closed, + "closed_arrival": rule.closed_arrival, + "closed_departure": rule.closed_departure, + } + rules_dict["rules"].extend(rules_room_type_index.values()) + return rules_dict, endpoint + + def pms_api_push_payload(self, payload, endpoint, client): + token = client.external_public_token + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "accept": "text/json", + } + response = requests.post(endpoint, headers=headers, data=json.dumps(payload)) + return response + + def generate_availability_json( + self, date_from, date_to, pms_property_id, room_type_id, client + ): + avail_records = self.env["pms.availability"].search( + [ + ("date", ">=", date_from), + ("date", "<=", date_to), + ("pms_property_id", "=", pms_property_id), + ("room_type_id", "=", room_type_id), + ], + order="date", + ) + avail_data = [] + current_avail = None + current_date_from = None + current_date_to = None + all_dates = [ + date_from + datetime.timedelta(days=x) + for x in range((date_to - date_from).days + 1) + ] + property_client_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", pms_property_id), + ("agency_id", "=", client.partner_id.id), + ] + ) + plan_avail = property_client_conf.main_avail_plan_id + for date in all_dates: + avail_record = avail_records.filtered(lambda r: r.date == date) + if avail_record: + avail_rule = avail_record.avail_rule_ids.filtered( + lambda r: r.availability_plan_id == plan_avail + ) + if avail_rule: + avail = avail_rule.plan_avail + else: + room_type = avail_rule.room_type_id + avail = min( + [ + avail_record.real_avail, + room_type.default_max_avail + if room_type.default_max_avail >= 0 + else avail_record.real_avail, + room_type.default_quota + if room_type.default_quota >= 0 + else avail_record.real_avail, + ] + ) + else: + room_type = self.env["pms.room.type"].browse(room_type_id) + avail = min( + [ + len( + room_type.room_ids.filtered( + lambda r: r.active + and r.pms_property_id.id == pms_property_id + ) + ), + room_type.default_max_avail + if room_type.default_max_avail >= 0 + else avail_record.real_avail, + room_type.default_quota + if room_type.default_quota >= 0 + else avail_record.real_avail, + ] + ) + if current_avail is None: + current_avail = avail + current_date_from = date + current_date_to = date + elif current_avail == avail: + current_date_to = date + else: + avail_data.append( + { + "date_from": datetime.datetime.strftime( + current_date_from, "%Y-%m-%d" + ), + "date_to": datetime.datetime.strftime( + current_date_to, "%Y-%m-%d" + ), + "roomTypeId": room_type_id, + "avail": current_avail, + } + ) + current_avail = avail + current_date_from = date + current_date_to = date + if current_avail is not None: + avail_data.append( + { + "date_from": datetime.datetime.strftime( + current_date_from, "%Y-%m-%d" + ), + "date_to": datetime.datetime.strftime(current_date_to, "%Y-%m-%d"), + "roomTypeId": room_type_id, + "avail": current_avail, + } + ) + return avail_data + + def generate_restrictions_json( + self, date_from, date_to, pms_property_id, room_type_id, client + ): + """ + Group by range of dates with the same restrictions + Output format: + rules_data: [ + { + 'date_from': '2023-08-01', + 'date_to': '2023-08-30', + 'roomTypeId': 2, + 'min_stay': 2, + 'max_stay': 6, + 'closed': false, + 'closed_arrival': false, + 'closed_departure': false + } + ] + """ + property_client_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", pms_property_id), + ("agency_id", "=", client.partner_id.id), + ] + ) + rules_records = self.env["pms.availability.plan.rule"].search( + [ + ("date", ">=", date_from), + ("date", "<=", date_to), + ("pms_property_id", "=", pms_property_id), + ("room_type_id", "=", room_type_id), + ( + "availability_plan_id", + "=", + property_client_conf.main_avail_plan_id.id, + ), + ], + order="date", + ) + rules_data = [] + current_rule = None + current_date_from = None + current_date_to = None + all_dates = [ + date_from + datetime.timedelta(days=x) + for x in range((date_to - date_from).days + 1) + ] + for date in all_dates: + rules_record = rules_records.filtered(lambda r: r.date == date) + if rules_record: + rule = rules_record[0] + else: + rule = None + if current_rule is None: + current_rule = rule + current_date_from = date + current_date_to = date + elif ( + rule + and current_rule.min_stay == rule.min_stay + and current_rule.max_stay == rule.max_stay + and current_rule.closed == rule.closed + and current_rule.closed_arrival == rule.closed_arrival + and current_rule.closed_departure == rule.closed_departure + ): + current_date_to = date + else: + if current_rule: + rules_data.append( + { + "date_from": datetime.datetime.strftime( + current_date_from, "%Y-%m-%d" + ), + "date_to": datetime.datetime.strftime( + current_date_to, "%Y-%m-%d" + ), + "roomTypeId": room_type_id, + "min_stay": current_rule.min_stay, + "max_stay": current_rule.max_stay, + "closed": current_rule.closed, + "closed_arrival": current_rule.closed_arrival, + "closed_departure": current_rule.closed_departure, + } + ) + current_rule = rule + current_date_from = date + current_date_to = date + return rules_data + + def generate_prices_json( + self, date_from, date_to, pms_property_id, room_type_id, client + ): + """ + prices: [ + { + 'date_from': '2023-07-02', + 'date_to': '2023-07-05', + 'roomTypeId': 2, + 'price': 50 + } + ] + """ + all_dates = [ + date_from + datetime.timedelta(days=x) + for x in range((date_to - date_from).days + 1) + ] + product = self.env["pms.room.type"].browse(room_type_id).product_id + property_client_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", pms_property_id), + ("agency_id", "=", client.partner_id.id), + ] + ) + pms_property = self.env["pms.property"].browse(pms_property_id) + pricelist = property_client_conf.main_pricelist_id + product_context = dict( + self.env.context, + date=datetime.datetime.today().date(), + pricelist=pricelist.id or pms_property.default_pricelist_id.id, + uom=product.uom_id.id, + fiscal_position=False, + property=pms_property_id, + ) + prices_data = [] + current_price = None + current_date_from = None + current_date_to = None + for index, date in enumerate(all_dates): + product_context["consumption_date"] = date + product = product.with_context(product_context) + price = round( + self.env["account.tax"]._fix_tax_included_price_company( + self.env["product.product"]._pms_get_display_price( + pricelist_id=pricelist.id, + product=product, + company_id=pms_property.company_id.id, + product_qty=1, + partner_id=False, + ), + product.taxes_id, + product.taxes_id, + pms_property.company_id, + ), + 2, + ) + if current_price is None: + current_price = price + current_date_from = date + current_date_to = date + elif current_price == price and index < len(all_dates) - 1: + current_date_to = date + else: + prices_data.append( + { + "date_from": datetime.datetime.strftime( + current_date_from, "%Y-%m-%d" + ), + "date_to": datetime.datetime.strftime( + current_date_to, "%Y-%m-%d" + ), + "roomTypeId": room_type_id, + "price": current_price, + } + ) + current_price = price + current_date_from = date + current_date_to = date + if current_price is not None: + prices_data.append( + { + "date_from": datetime.datetime.strftime( + current_date_from, "%Y-%m-%d" + ), + "date_to": datetime.datetime.strftime(current_date_to, "%Y-%m-%d"), + "roomTypeId": room_type_id, + "price": current_price, + } + ) + return prices_data + + @api.model + def pms_api_push_batch( + self, + call_type, + date_from=lambda: datetime.datetime.today().date(), + date_to=lambda: datetime.datetime.today().date() + datetime.timedelta(days=365), + filter_room_type_id=False, + pms_property_codes=False, + client=False, + ): + if client: + 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() + if date_from < datetime.datetime.today().date(): + date_from = datetime.datetime.today().date() + if isinstance(date_to, str): + date_to = datetime.datetime.strptime(date_to, "%Y-%m-%d").date() + if date_to <= date_from: + date_to = date_from + for client in clients: + if not pms_property_codes: + pms_properties = client.pms_property_ids + else: + pms_properties = self.env["pms.property"].search( + [ + ("pms_property_code", "in", pms_property_codes), + ("id", "in", client.pms_property_ids.ids), + ] + ) + for pms_property in pms_properties: + try: + property_client_conf = ( + self.env["ota.property.settings"] + .sudo() + .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 + ) + .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, + } + ) diff --git a/pms_api_rest/models/pms_reservation.py b/pms_api_rest/models/pms_reservation.py new file mode 100644 index 0000000000..cd43a1e27c --- /dev/null +++ b/pms_api_rest/models/pms_reservation.py @@ -0,0 +1,11 @@ +from odoo import api, models + + +class PmsReservation(models.Model): + _inherit = "pms.reservation" + + @api.model + def create(self, vals): + result = super(PmsReservation, self).create(vals) + result.access_token = result._portal_ensure_token() + return result diff --git a/pms_api_rest/models/pms_room_type_class.py b/pms_api_rest/models/pms_room_type_class.py new file mode 100644 index 0000000000..ddd4c81a2b --- /dev/null +++ b/pms_api_rest/models/pms_room_type_class.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class PmsRoomTypeClass(models.Model): + _inherit = "pms.room.type.class" + + icon_pms_api_rest = fields.Image( + string="Icon room type class image", + store=True, + ) diff --git a/pms_api_rest/models/product_template.py b/pms_api_rest/models/product_template.py new file mode 100644 index 0000000000..54cf34b0da --- /dev/null +++ b/pms_api_rest/models/product_template.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + channel_available = fields.Boolean( + string="Sale Channel Available", + help="If checked, the product will be available for Channel", + default=False, + ) diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py new file mode 100644 index 0000000000..85e1745636 --- /dev/null +++ b/pms_api_rest/models/res_users.py @@ -0,0 +1,55 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + availability_rule_field_ids = fields.Many2many( + string="Availability Rules", + help="Configurable availability rules", + comodel_name="ir.model.fields", + default=lambda self: self._get_default_avail_rule_fields(), + relation="ir_model_fields_res_users_rel", + column1="ir_model_fields", + column2="res_users", + ) + + is_new_interface_app_user = fields.Boolean( + string="Is New Interface App User", + help="Is New Interface App User", + default=False, + store=True, + readonly=False, + ) + pms_api_client = fields.Boolean( + string="PMS API Client", + help="PMS API Client", + ) + url_endpoint_prices = fields.Char( + string="URL Endpoint Prices", + help="URL Endpoint Prices", + ) + url_endpoint_availability = fields.Char( + string="URL Endpoint Availability", + help="URL Endpoint Availability", + ) + url_endpoint_rules = fields.Char( + string="URL Endpoint Rules", + help="URL Endpoint Rules", + ) + external_public_token = fields.Char( + string="External Public Token", + help="External Public Token", + ) + + def _get_default_avail_rule_fields(self): + default_avail_rule_fields = self.env["ir.model.fields"].search( + [ + ("model_id", "=", "pms.availability.plan.rule"), + ("name", "in", ("min_stay", "quota")), + ] + ) + if default_avail_rule_fields: + return default_avail_rule_fields.ids + else: + return [] diff --git a/pms_api_rest/models/sql_export.py b/pms_api_rest/models/sql_export.py new file mode 100644 index 0000000000..26c26b8a12 --- /dev/null +++ b/pms_api_rest/models/sql_export.py @@ -0,0 +1,15 @@ +from odoo import _, models +from odoo.exceptions import UserError + + +class SqlExport(models.Model): + _inherit = "sql.export" + + def unlink(self): + if ( + self.env.ref("pms_api_rest.sql_export_services") in self + or self.env.ref("pms_api_rest.sql_export_departures") in self + or self.env.ref("pms_api_rest.sql_export_arrivals") in self + ): + raise UserError(_("You can not delete PMS SQL query")) + return super().unlink() diff --git a/pms_api_rest/pms_api_rest_utils.py b/pms_api_rest/pms_api_rest_utils.py new file mode 100644 index 0000000000..6a13093f1e --- /dev/null +++ b/pms_api_rest/pms_api_rest_utils.py @@ -0,0 +1,25 @@ +from odoo.http import request + + +def url_image_pms_api_rest(model, record_id, field): + rt_image_attach = ( + request.env["ir.attachment"] + .sudo() + .search( + [ + ("res_model", "=", model), + ("res_id", "=", record_id), + ("res_field", "=", field), + ] + ) + ) + if rt_image_attach and not rt_image_attach.access_token: + rt_image_attach.generate_access_token() + result = ( + request.env["ir.config_parameter"].sudo().get_param("web.base.url") + + "/web/image/%s?access_token=%s" + % (rt_image_attach.id, rt_image_attach.access_token) + if rt_image_attach + else False + ) + return result if result else "" diff --git a/pms_api_rest/security/ir.model.access.csv b/pms_api_rest/security/ir.model.access.csv new file mode 100644 index 0000000000..bb8bad458f --- /dev/null +++ b/pms_api_rest/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +user_access_ota_property_settings,user_access_ota_property_settings,model_ota_property_settings,pms.group_pms_user,1,0,0,0 +manager_access_ota_property_settings,manager_access_ota_property_settings,model_ota_property_settings,pms.group_pms_manager,1,1,1,1 +techinal_pms_api_log_access,techinal_pms_api_log_access,model_pms_api_log,base.group_system,1,1,1,1 diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py new file mode 100644 index 0000000000..be5f217582 --- /dev/null +++ b/pms_api_rest/services/__init__.py @@ -0,0 +1,47 @@ +from . import pms_folio_service +from . import pms_room_service +from . import pms_room_type_service +from . import pms_calendar_service +from . import pms_partner_service + +from . import pms_reservation_service +from . import pms_reservation_line_service +from . import pms_property_service +from . import pms_login_service +from . import pms_pricelist_service +from . import pms_price_service +from . import pms_availability_plan_service +from . import pms_id_category_service +from . import res_country_service +from . import res_partner_category_service +from . import res_city_zip_service +from . import pms_room_type_class_service +from . import pms_ubication_service +from . import pms_extra_bed_service + +from . import pms_amenity_service +from . import pms_amenity_type_service + +from . import pms_board_service_line_service +from . import pms_board_service_service + +from . import pms_product_service +from . import pms_sale_channel_service +from . import pms_cancelation_rule_service + +from . import pms_agency_service +from . import pms_service_service +from . import pms_service_line_service +from . import pms_room_closure_reason_service + +from . import res_lang_service +from . import pms_transaction_service +from . import pms_account_payment_terms_service +from . import pms_account_journal_service +from . import pms_invoice_service +from . import pms_notification_service +from . import pms_avail_service +from . import pms_user_service +from . import pms_dashboard_service +from . import feed_post_service +from . import ocr_document_service diff --git a/pms_api_rest/services/feed_post_service.py b/pms_api_rest/services/feed_post_service.py new file mode 100644 index 0000000000..3b4c832b2b --- /dev/null +++ b/pms_api_rest/services/feed_post_service.py @@ -0,0 +1,39 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsFeedRss(Component): + _inherit = "base.rest.service" + _name = "pms.feed.rss.service" + _usage = "feed-posts" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("feed.post.info", is_list=True), + auth="jwt_api_pms", + ) + def get_feed_posts(self): + result_rss = [] + PmsFeedRss = self.env.datamodels["feed.post.info"] + for rss in self.env["rss.post"].search([], limit=5, order="publish_date desc"): + result_rss.append( + PmsFeedRss( + postId=rss.post_id, + title=rss.title, + link=rss.link, + description=rss.description, + publishDate=str(rss.publish_date), + author=rss.author if rss.author else "", + imageUrl=rss.image_url or "", + ) + ) + return result_rss diff --git a/pms_api_rest/services/ocr_document_service.py b/pms_api_rest/services/ocr_document_service.py new file mode 100644 index 0000000000..f1e9941e7f --- /dev/null +++ b/pms_api_rest/services/ocr_document_service.py @@ -0,0 +1,140 @@ +from datetime import datetime + +from odoo import _ +from odoo.exceptions import AccessError, MissingError + +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.addons.portal.controllers.portal import CustomerPortal + + +class PmsOcr(Component): + _inherit = "base.rest.service" + _name = "ocr.document.service" + _usage = "ocr-document" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.ocr.input"), + output_param=Datamodel("pms.ocr.checkin.result", is_list=False), + auth="jwt_api_pms", + ) + def process_ocr_document(self, input_param): + pms_property = self.env["pms.property"].browse(input_param.pmsPropertyId) + ocr_find_method_name = ( + "_%s_document_process" % pms_property.ocr_checkin_supplier + ) + if hasattr(pms_property, ocr_find_method_name): + checkin_data_dict = getattr(pms_property, ocr_find_method_name)( + input_param.imageBase64Front, input_param.imageBase64Back + ) + PmsOcrCheckinResult = self.env.datamodels["pms.ocr.checkin.result"] + + return PmsOcrCheckinResult( + nationality=checkin_data_dict.get("nationality") or None, + countryId=checkin_data_dict.get("country_id") or None, + firstname=checkin_data_dict.get("firstname") or None, + lastname=checkin_data_dict.get("lastname") or None, + lastname2=checkin_data_dict.get("lastname2") or None, + gender=checkin_data_dict.get("gender") or None, + birthdate=datetime.strftime( + checkin_data_dict.get("birthdate"), "%Y-%m-%dT%H:%M:%S" + ) + if checkin_data_dict.get("birthdate") + else None, + documentType=checkin_data_dict.get("document_type") or None, + documentExpeditionDate=datetime.strftime( + checkin_data_dict.get("document_expedition_date"), "%Y-%m-%dT%H:%M:%S" + ) + if checkin_data_dict.get("document_expedition_date") + else None, + documentSupportNumber=checkin_data_dict.get("document_support_number") + or None, + documentNumber=checkin_data_dict.get("document_number") or None, + residenceStreet=checkin_data_dict.get("residence_street") or None, + residenceCity=checkin_data_dict.get("residence_city") or None, + countryState=checkin_data_dict.get("country_state") or None, + documentCountryId=checkin_data_dict.get("document_country_id") or None, + zip=checkin_data_dict.get("zip") or None, + ) + + @restapi.method( + [ + ( + [ + "//precheckin-reservation/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.ocr.input"), + output_param=Datamodel("pms.ocr.checkin.result", is_list=False), + auth="public", + ) + def process_ocr_document_public(self, reservation_id, token, input_param): + # check if the reservation exists + reservation_record = ( + self.env["pms.reservation"] + .sudo() + .browse(reservation_id) + ) + if not reservation_record.exists(): + raise MissingError(_("Reservation not found")) + + # check if the reservation is accessible + try: + CustomerPortal._document_check_access( + self, + "pms.reservation", + reservation_record.id, + access_token=token, + ) + except AccessError: + raise MissingError(_("Reservation not found")) + + pms_property = self.env["pms.property"].sudo().browse(input_param.pmsPropertyId) + ocr_find_method_name = ( + "_%s_document_process" % pms_property.ocr_checkin_supplier + ) + if hasattr(pms_property, ocr_find_method_name): + checkin_data_dict = getattr(pms_property, ocr_find_method_name)( + input_param.imageBase64Front, input_param.imageBase64Back + ) + PmsOcrCheckinResult = self.env.datamodels["pms.ocr.checkin.result"] + + return PmsOcrCheckinResult( + nationality=checkin_data_dict.get("nationality") or None, + countryId=checkin_data_dict.get("country_id") or None, + firstname=checkin_data_dict.get("firstname") or None, + lastname=checkin_data_dict.get("lastname") or None, + lastname2=checkin_data_dict.get("lastname2") or None, + gender=checkin_data_dict.get("gender") or None, + birthdate=datetime.strftime( + checkin_data_dict.get("birthdate"), "%Y-%m-%dT%H:%M:%S" + ) + if checkin_data_dict.get("birthdate") + else None, + documentType=checkin_data_dict.get("document_type") or None, + documentExpeditionDate=datetime.strftime( + checkin_data_dict.get("document_expedition_date"), "%Y-%m-%dT%H:%M:%S" + ) + if checkin_data_dict.get("document_expedition_date") + else None, + documentSupportNumber=checkin_data_dict.get("document_support_number") + or None, + documentNumber=checkin_data_dict.get("document_number") or None, + residenceStreet=checkin_data_dict.get("residence_street") or None, + residenceCity=checkin_data_dict.get("residence_city") or None, + countryState=checkin_data_dict.get("country_state") or None, + documentCountryId=checkin_data_dict.get("document_country_id") or None, + zip=checkin_data_dict.get("zip") or None, + ) diff --git a/pms_api_rest/services/pms_account_journal_service.py b/pms_api_rest/services/pms_account_journal_service.py new file mode 100644 index 0000000000..8e66c2369f --- /dev/null +++ b/pms_api_rest/services/pms_account_journal_service.py @@ -0,0 +1,49 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsAccountJournalService(Component): + _inherit = "base.rest.service" + _name = "pms.account.journal.service" + _usage = "account-journals" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.account.journal.search.param"), + output_param=Datamodel("pms.account.journal.info", is_list=True), + auth="jwt_api_pms", + ) + def get_method_payments(self, account_journal_search_param): + pms_property = self.env["pms.property"].search( + [("id", "=", account_journal_search_param.pmsPropertyId)] + ) + PmsAccountJournalInfo = self.env.datamodels["pms.account.journal.info"] + result_account_journals = [] + if not pms_property: + pass + else: + for payment_method in pms_property._get_payment_methods( + automatic_included=True + ): + # REVIEW: avoid send to app generic company journals + if not payment_method.pms_property_ids: + continue + result_account_journals.append( + PmsAccountJournalInfo( + id=payment_method.id, + name=payment_method.name, + type=payment_method.type, + allowedPayments=payment_method.allowed_pms_payments, + ) + ) + + return result_account_journals diff --git a/pms_api_rest/services/pms_account_payment_terms_service.py b/pms_api_rest/services/pms_account_payment_terms_service.py new file mode 100644 index 0000000000..1589d00142 --- /dev/null +++ b/pms_api_rest/services/pms_account_payment_terms_service.py @@ -0,0 +1,37 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsAccountPaymentTermService(Component): + _inherit = "base.rest.service" + _name = "pms.account.payment.term.service" + _usage = "payment-terms" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.account.transaction.term.info", is_list=True), + auth="jwt_api_pms", + ) + def get_account_payment_terms(self): + + PmsAccountPaymenttermInfo = self.env.datamodels[ + "pms.account.transaction.term.info" + ] + res = [] + for payment_term in self.env["account.payment.term"].search([]): + res.append( + PmsAccountPaymenttermInfo( + id=payment_term.id, + name=payment_term.name, + ) + ) + return res diff --git a/pms_api_rest/services/pms_agency_service.py b/pms_api_rest/services/pms_agency_service.py new file mode 100644 index 0000000000..f5e48a1975 --- /dev/null +++ b/pms_api_rest/services/pms_agency_service.py @@ -0,0 +1,80 @@ +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +from ..pms_api_rest_utils import url_image_pms_api_rest + + +class PmsAgencyService(Component): + _inherit = "base.rest.service" + _name = "pms.agency.service" + _usage = "agencies" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.agency.search.param"), + output_param=Datamodel("pms.agency.info", is_list=True), + auth="jwt_api_pms", + ) + def get_agencies(self, agencies_search_param): + domain = [("is_agency", "=", True)] + if agencies_search_param.otas: + domain.append(("sale_channel_id.is_on_line", "=", True)) + if agencies_search_param.name: + domain.append(("name", "like", agencies_search_param.name)) + result_agencies = [] + PmsAgencyInfo = self.env.datamodels["pms.agency.info"] + for agency in self.env["res.partner"].search( + domain, + ): + + result_agencies.append( + PmsAgencyInfo( + id=agency.id, + name=agency.name, + imageUrl=url_image_pms_api_rest( + "res.partner", agency.id, "image_128" + ), + ) + ) + return result_agencies + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.agency.info", is_list=False), + auth="jwt_api_pms", + ) + def get_agency(self, agency_id): + agency = self.env["res.partner"].search( + [ + ("id", "=", agency_id), + ("is_agency", "=", True), + ] + ) + if agency: + PmsAgencieInfo = self.env.datamodels["pms.agency.info"] + return PmsAgencieInfo( + id=agency.id, + name=agency.name if agency.name else None, + imageUrl=url_image_pms_api_rest("res.partner", agency.id, "image_128"), + ) + else: + raise MissingError(_("Agency not found")) diff --git a/pms_api_rest/services/pms_amenity_service.py b/pms_api_rest/services/pms_amenity_service.py new file mode 100644 index 0000000000..161a6edbfd --- /dev/null +++ b/pms_api_rest/services/pms_amenity_service.py @@ -0,0 +1,83 @@ +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsAmenityService(Component): + _inherit = "base.rest.service" + _name = "pms.amenity.service" + _usage = "amenities" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.amenity.search.param"), + output_param=Datamodel("pms.amenity.info", is_list=True), + auth="jwt_api_pms", + ) + def get_amenities(self, amenities_search_param): + domain = [("pms_amenity_type_id", "!=", False)] + if amenities_search_param.name: + domain.append(("name", "like", amenities_search_param.name)) + if amenities_search_param.pmsPropertyId: + domain.extend( + [ + "|", + ("pms_property_ids", "in", amenities_search_param.pmsPropertyId), + ("pms_property_ids", "=", False), + ] + ) + + result_amenities = [] + PmsAmenityInfo = self.env.datamodels["pms.amenity.info"] + for amenity in self.env["pms.amenity"].search( + domain, + ): + + result_amenities.append( + PmsAmenityInfo( + id=amenity.id, + name=amenity.name, + amenityTypeId=amenity.pms_amenity_type_id.id, + addInRoomName=amenity.is_add_code_room_name, + defaultCode=amenity.default_code if amenity.default_code else None, + ) + ) + return result_amenities + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.amenity.info", is_list=False), + auth="jwt_api_pms", + ) + def get_amenity(self, amenity_id): + amenity = self.env["pms.amenity"].search([("id", "=", amenity_id)]) + if amenity: + PmsAmenityInfo = self.env.datamodels["pms.amenity.info"] + return PmsAmenityInfo( + id=amenity.id, + name=amenity.name, + defaultCode=amenity.default_code if amenity.default_code else None, + amenityTypeId=amenity.pms_amenity_type_id.id + if amenity.pms_amenity_type_id + else None, + ) + else: + raise MissingError(_("Amenity not found")) diff --git a/pms_api_rest/services/pms_amenity_type_service.py b/pms_api_rest/services/pms_amenity_type_service.py new file mode 100644 index 0000000000..cd9f7e0dac --- /dev/null +++ b/pms_api_rest/services/pms_amenity_type_service.py @@ -0,0 +1,82 @@ +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsAmenityTypeService(Component): + _inherit = "base.rest.service" + _name = "pms.amenity.type.service" + _usage = "amenity-types" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.amenity.type.search.param"), + output_param=Datamodel("pms.amenity.type.info", is_list=True), + auth="jwt_api_pms", + ) + def get_amenity_types(self, amenity_types_search_param): + domain = [] + if amenity_types_search_param.name: + domain.append(("name", "like", amenity_types_search_param.name)) + if amenity_types_search_param.pmsPropertyId: + domain.extend( + [ + "|", + ( + "pms_property_ids", + "in", + amenity_types_search_param.pmsPropertyId, + ), + ("pms_property_ids", "=", False), + ] + ) + + result_amenity_types = [] + PmsAmenityTypeInfo = self.env.datamodels["pms.amenity.type.info"] + for amenity_type in self.env["pms.amenity.type"].search( + domain, + ): + + result_amenity_types.append( + PmsAmenityTypeInfo( + id=amenity_type.id, + name=amenity_type.name, + ) + ) + return result_amenity_types + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.amenity.type.info", is_list=False), + auth="jwt_api_pms", + ) + def get_amenity_type(self, amenity_type_id): + amenity_type = self.env["pms.amenity.type"].search( + [("id", "=", amenity_type_id)] + ) + if amenity_type: + PmsAmenityTypeInfo = self.env.datamodels["pms.amenity.type.info"] + return PmsAmenityTypeInfo( + id=amenity_type.id, + name=amenity_type.name, + ) + else: + raise MissingError(_("Amenity Type not found")) diff --git a/pms_api_rest/services/pms_avail_service.py b/pms_api_rest/services/pms_avail_service.py new file mode 100644 index 0000000000..ec10ca4e1a --- /dev/null +++ b/pms_api_rest/services/pms_avail_service.py @@ -0,0 +1,65 @@ +from datetime import datetime, timedelta + +from odoo import _, fields +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsAvailService(Component): + _inherit = "base.rest.service" + _name = "pms.avail.service" + _usage = "avails" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.avail.search.param"), + output_param=Datamodel("pms.avail.info", is_list=True), + auth="jwt_api_pms", + ) + def get_avails(self, avails_search_param): + if not ( + avails_search_param.availabilityFrom + and avails_search_param.availabilityTo + and avails_search_param.pmsPropertyId + ): + raise MissingError(_("Missing required parameters")) + pricelist_id = avails_search_param.pricelistId or False + room_type_id = avails_search_param.roomTypeId or False + pms_property = self.env["pms.property"].browse( + avails_search_param.pmsPropertyId + ) + PmsAvailInfo = self.env.datamodels["pms.avail.info"] + result_avails = [] + date_from = fields.Date.from_string(avails_search_param.availabilityFrom) + date_to = fields.Date.from_string(avails_search_param.availabilityTo) + dates = [ + date_from + timedelta(days=x) + for x in range(0, (date_to - date_from).days + 1) + ] + for item_date in dates: + pms_property = pms_property.with_context( + checkin=item_date, + checkout=item_date + timedelta(days=1), + room_type_id=room_type_id, + current_lines=avails_search_param.currentLines or False, + pricelist_id=pricelist_id, + real_avail=True, + ) + result_avails.append( + PmsAvailInfo( + date=datetime.combine(item_date, datetime.min.time()).isoformat(), + roomIds=pms_property.free_room_ids.ids, + ) + ) + return result_avails diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py new file mode 100644 index 0000000000..17416f5114 --- /dev/null +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -0,0 +1,276 @@ +from datetime import datetime, timedelta + +from odoo import _ +from odoo.exceptions import MissingError, ValidationError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsAvailabilityPlanService(Component): + _inherit = "base.rest.service" + _name = "pms.availability.plan.service" + _usage = "availability-plans" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.search.param", is_list=False), + output_param=Datamodel("pms.availability.plan.info", is_list=True), + auth="jwt_api_pms", + ) + def get_availability_plans(self, pms_search_param, **args): + + availability_plans_all_properties = self.env["pms.availability.plan"].search( + [("pms_property_ids", "=", False)] + ) + availabilities = set() + if pms_search_param.pmsPropertyIds: + for index, prop in enumerate(pms_search_param.pmsPropertyIds): + availabilities_with_query_property = self.env[ + "pms.availability.plan" + ].search([("pms_property_ids", "=", prop)]) + if index == 0: + availabilities = set(availabilities_with_query_property.ids) + else: + availabilities = availabilities.intersection( + set(availabilities_with_query_property.ids) + ) + availabilities_total = list( + set(list(availabilities) + availability_plans_all_properties.ids) + ) + else: + availabilities_total = list(availability_plans_all_properties.ids) + domain = [ + ("id", "in", availabilities_total), + ] + + PmsAvialabilityPlanInfo = self.env.datamodels["pms.availability.plan.info"] + result_availabilities = [] + for availability in self.env["pms.availability.plan"].search(domain): + result_availabilities.append( + PmsAvialabilityPlanInfo( + id=availability.id, + name=availability.name, + pmsPropertyIds=availability.pms_property_ids.mapped("id"), + ) + ) + return result_availabilities + + @restapi.method( + [ + ( + [ + "//availability-plan-rules", + ], + "GET", + ) + ], + input_param=Datamodel("pms.availability.plan.rule.search.param", is_list=False), + output_param=Datamodel("pms.availability.plan.rule.info", is_list=True), + auth="jwt_api_pms", + ) + def get_availability_plan_rules( + self, availability_plan_id, availability_plan_rule_search_param + ): + date_from = datetime.strptime( + availability_plan_rule_search_param.dateFrom, "%Y-%m-%d" + ).date() + date_to = datetime.strptime( + availability_plan_rule_search_param.dateTo, "%Y-%m-%d" + ).date() + count_nights = (date_to - date_from).days + 1 + target_dates = [date_from + timedelta(days=x) for x in range(count_nights)] + pms_property_id = availability_plan_rule_search_param.pmsPropertyId + record_availability_plan_id = self.env["pms.availability.plan"].browse( + availability_plan_id + ) + if not record_availability_plan_id: + raise MissingError + + rooms = ( + self.env["pms.room"] + .with_context(active_test=True) + .search( + [ + ( + "pms_property_id", + "=", + availability_plan_rule_search_param.pmsPropertyId, + ) + ] + ) + ) + room_type_ids = rooms.mapped("room_type_id").ids + selected_fields = [ + "id", + "date", + "room_type_id", + "min_stay", + "min_stay_arrival", + "max_stay", + "max_stay_arrival", + "closed", + "closed_departure", + "closed_arrival", + "quota", + "max_avail", + ] + sql_select = "SELECT %s" % ", ".join(selected_fields) + self.env.cr.execute( + f""" + {sql_select} + FROM pms_availability_plan_rule rule + WHERE (pms_property_id = %s) + AND (date in %s) + AND (availability_plan_id = %s) + AND (room_type_id in %s) + """, + ( + pms_property_id, + tuple(target_dates), + record_availability_plan_id.id, + tuple(room_type_ids), + ), + ) + result_sql = self.env.cr.fetchall() + rules = [] + for res in result_sql: + rules.append( + {field: res[selected_fields.index(field)] for field in selected_fields} + ) + + result = [] + PmsAvailabilityPlanRuleInfo = self.env.datamodels[ + "pms.availability.plan.rule.info" + ] + + for date in target_dates: + for room_type_id in room_type_ids: + rule = next( + ( + rule + for rule in rules + if rule["room_type_id"] == room_type_id and rule["date"] == date + ), + False, + ) + + if rule: + availability_plan_rule_info = PmsAvailabilityPlanRuleInfo( + roomTypeId=rule["room_type_id"], + date=datetime.combine(date, datetime.min.time()).isoformat(), + availabilityRuleId=rule["id"], + minStay=rule["min_stay"], + minStayArrival=rule["min_stay_arrival"], + maxStay=rule["max_stay"], + maxStayArrival=rule["max_stay_arrival"], + closed=rule["closed"], + closedDeparture=rule["closed_departure"], + closedArrival=rule["closed_arrival"], + quota=rule["quota"], + maxAvailability=rule["max_avail"], + availabilityPlanId=availability_plan_id, + ) + result.append(availability_plan_rule_info) + + return result + + def _create_or_update_avail_plan_rules(self, pms_avail_plan_rules_info): + for avail_plan_rule in pms_avail_plan_rules_info.availabilityPlanRules: + vals = dict() + date = datetime.strptime(avail_plan_rule.date, "%Y-%m-%d").date() + if avail_plan_rule.minStay is not None: + vals.update({"min_stay": avail_plan_rule.minStay}) + if avail_plan_rule.minStayArrival is not None: + vals.update({"min_stay_arrival": avail_plan_rule.minStayArrival}) + if avail_plan_rule.maxStay is not None: + vals.update({"max_stay": avail_plan_rule.maxStay}) + if avail_plan_rule.maxStayArrival is not None: + vals.update({"max_stay_arrival": avail_plan_rule.maxStayArrival}) + if avail_plan_rule.closed is not None: + vals.update({"closed": avail_plan_rule.closed}) + if avail_plan_rule.closedDeparture is not None: + vals.update({"closed_departure": avail_plan_rule.closedDeparture}) + if avail_plan_rule.closedArrival is not None: + vals.update({"closed_arrival": avail_plan_rule.closedArrival}) + if avail_plan_rule.quota is not None: + vals.update({"quota": avail_plan_rule.quota}) + if avail_plan_rule.maxAvailability is not None: + vals.update({"max_avail": avail_plan_rule.maxAvailability}) + avail_rule = self.env["pms.availability.plan.rule"].search( + [ + ("availability_plan_id", "=", avail_plan_rule.availabilityPlanId), + ("pms_property_id", "=", avail_plan_rule.pmsPropertyId), + ("room_type_id", "=", avail_plan_rule.roomTypeId), + ("date", "=", date), + ] + ) + if avail_rule: + avail_rule.write(vals) + else: + vals.update( + { + "room_type_id": avail_plan_rule.roomTypeId, + "date": date, + "pms_property_id": avail_plan_rule.pmsPropertyId, + "availability_plan_id": avail_plan_rule.availabilityPlanId, + } + ) + self.env["pms.availability.plan.rule"].create(vals) + + @restapi.method( + [ + ( + [ + "/p//availability-plan-rules", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.availability.plan.rules.info", is_list=False), + auth="jwt_api_pms", + ) + def create_availability_plan_rule( + self, availability_plan_id, pms_avail_plan_rules_info + ): + availability_plan_ids = list( + { + item.availabilityPlanId + for item in pms_avail_plan_rules_info.availabilityPlanRules + } + ) + if ( + len(availability_plan_ids) > 1 + or availability_plan_ids[0] != availability_plan_id + ): + raise ValidationError( + _( + "You cannot create availability plan rules for different availability plans" + ) + ) + else: + self._create_or_update_avail_plan_rules(pms_avail_plan_rules_info) + + @restapi.method( + [ + ( + [ + "/batch-changes", + ], + "POST", + ) + ], + input_param=Datamodel("pms.availability.plan.rules.info", is_list=False), + auth="jwt_api_pms", + ) + def update_availability_plan_rules(self, pms_avail_plan_rules_info): + self._create_or_update_avail_plan_rules(pms_avail_plan_rules_info) diff --git a/pms_api_rest/services/pms_board_service_line_service.py b/pms_api_rest/services/pms_board_service_line_service.py new file mode 100644 index 0000000000..5aeba45ada --- /dev/null +++ b/pms_api_rest/services/pms_board_service_line_service.py @@ -0,0 +1,81 @@ +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsBoardServiceService(Component): + _inherit = "base.rest.service" + _name = "pms.board.service.line.service" + _usage = "board-service-lines" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.board.service.line.search.param"), + output_param=Datamodel("pms.board.service.line.info", is_list=True), + auth="jwt_api_pms", + ) + def get_board_service_lines(self, board_service_lines_search_param): + domain = [] + if board_service_lines_search_param.boardServiceId: + domain.append( + ( + "pms_board_service_room_type_id", + "=", + board_service_lines_search_param.boardServiceId, + ) + ) + + result_board_service_lines = [] + PmsBoardServiceInfo = self.env.datamodels["pms.board.service.line.info"] + for line in self.env["pms.board.service.room.type.line"].search( + domain, + ): + result_board_service_lines.append( + PmsBoardServiceInfo( + id=line.id, + name=line.pms_board_service_room_type_id.display_name, + boardServiceId=line.pms_board_service_room_type_id.id, + productId=line.product_id.id, + amount=round(line.amount, 2), + ) + ) + return result_board_service_lines + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.board.service.line.info", is_list=False), + auth="jwt_api_pms", + ) + def get_board_service_line(self, board_service_line_id): + board_service_line = self.env["pms.board.service.room.type.line"].search( + [("id", "=", board_service_line_id)] + ) + if board_service_line: + PmsBoardServiceInfo = self.env.datamodels["pms.board.service.line.info"] + return PmsBoardServiceInfo( + id=board_service_line.id, + name=board_service_line.pms_board_service_room_type_id.display_name, + boardServiceId=board_service_line.pms_board_service_room_type_id.id, + productId=board_service_line.product_id.id, + amount=round(board_service_line.amount, 2), + ) + else: + raise MissingError(_("Board service line not found")) diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py new file mode 100644 index 0000000000..5b5acb2cdd --- /dev/null +++ b/pms_api_rest/services/pms_board_service_service.py @@ -0,0 +1,172 @@ +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsBoardServiceService(Component): + _inherit = "base.rest.service" + _name = "pms.board.service.service" + _usage = "board-services" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.board.service.search.param"), + output_param=Datamodel("pms.board.service.info", is_list=True), + auth="jwt_api_pms", + ) + def get_board_services(self, board_services_search_param): + external_app = self.env.user.pms_api_client + domain = [] + if board_services_search_param.name: + domain.append(("name", "like", board_services_search_param.name)) + if board_services_search_param.roomTypeId: + domain.append( + ("pms_room_type_id", "=", board_services_search_param.roomTypeId) + ) + if board_services_search_param.pmsPropertyId: + domain.extend( + [ + ("pms_property_id", "=", board_services_search_param.pmsPropertyId), + ] + ) + + result_board_services = [] + PmsBoardServiceInfo = self.env.datamodels["pms.board.service.info"] + for board_service in self.env["pms.board.service.room.type"].search( + domain, + ): + result_board_services.append( + PmsBoardServiceInfo( + id=board_service.id, + name=board_service.pms_board_service_id.display_name, + roomTypeId=board_service.pms_room_type_id.id, + amount=round(board_service.amount, 2), + boardServiceId=board_service.pms_board_service_id, + boardServiceLineIds=board_service.board_service_line_ids.ids, + ) + ) + if external_app: + room_type_ids = board_services_search_param.roomTypeId or self.env[ + "pms.room" + ].search( + [("pms_property_id", "=", board_services_search_param.pmsPropertyId)] + ).mapped( + "room_type_id.id" + ) + for room_type_id in room_type_ids: + result_board_services.append( + PmsBoardServiceInfo( + id=0, + name="Solo Alojamiento", + roomTypeId=room_type_id, + amount=0, + boardServiceId=0, + boardServiceLineIds=[], + ) + ) + return result_board_services + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.board.service.info", is_list=False), + auth="jwt_api_pms", + ) + def get_board_service(self, board_service_id): + board_service = self.env["pms.board.service.room.type"].search( + [("id", "=", board_service_id)] + ) + if board_service: + PmsBoardServiceInfo = self.env.datamodels["pms.board.service.info"] + return PmsBoardServiceInfo( + id=board_service.id, + name=board_service.pms_board_service_id.display_name, + roomTypeId=board_service.pms_room_type_id.id, + amount=round(board_service.amount), + boardServiceLineIds=board_service.board_service_line_ids.ids, + ) + else: + raise MissingError(_("Board Service not found")) + + @restapi.method( + [ + ( + [ + "//lines", + ], + "GET", + ) + ], + input_param=Datamodel("pms.search.param"), + output_param=Datamodel("pms.board.service.line.info", is_list=True), + auth="jwt_api_pms", + ) + def get_board_service_lines(self, board_service_id, pms_search_param): + domain = list() + domain.append(("pms_board_service_room_type_id", "=", board_service_id)) + if pms_search_param.pmsPropertyId: + domain.extend( + [ + ("pms_property_id", "=", pms_search_param.pmsPropertyId), + ] + ) + result_board_service_lines = [] + PmsBoardServiceLineInfo = self.env.datamodels["pms.board.service.line.info"] + for line in self.env["pms.board.service.room.type.line"].search( + domain, + ): + result_board_service_lines.append( + PmsBoardServiceLineInfo( + id=line.id, + name=line.pms_board_service_room_type_id.display_name, + boardServiceId=line.pms_board_service_room_type_id.id, + productId=line.product_id.id, + amount=round(line.amount, 2), + isAdults=line.adults, + isChildren=line.children, + ) + ) + return result_board_service_lines + + @restapi.method( + [ + ( + [ + "/restricted/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.board.service.info", is_list=False), + auth="jwt_api_pms", + ) + def get_board_service(self, board_service_id): + board_service_record = self.env["pms.board.service.room.type"].sudo().browse(board_service_id) + if board_service_record.exists(): + PmsBoardServiceInfo = self.env.datamodels["pms.board.service.info"] + return PmsBoardServiceInfo( + id=board_service_record.id, + name=board_service_record.pms_board_service_id.display_name, + roomTypeId=board_service_record.pms_room_type_id.id, + amount=round(board_service_record.amount), + productIds=board_service_record.board_service_line_ids.mapped("product_id.id"), + ) + else: + raise MissingError(_("Board Service not found")) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py new file mode 100644 index 0000000000..6d4b14045a --- /dev/null +++ b/pms_api_rest/services/pms_calendar_service.py @@ -0,0 +1,1012 @@ +from datetime import datetime, timedelta + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +def build_reservation_line_info(calendar_item, previous_item=False, next_item=False): + next_itemSplitted = ( + calendar_item["splitted"] + and next_item + and calendar_item["date"] < calendar_item["checkout"] - timedelta(days=1) + and ( + next_item["room_id"] != calendar_item["room_id"] + or next_item["reservation_id"] != calendar_item["reservation_id"] + ) + ) + previous_itemSplitted = ( + calendar_item["splitted"] + and previous_item + and calendar_item["date"] > calendar_item["checkin"] + and ( + previous_item["room_id"] != calendar_item["room_id"] + or previous_item["reservation_id"] != calendar_item["reservation_id"] + ) + ) + return { + "date": datetime.combine( + calendar_item["date"], datetime.min.time() + ).isoformat(), + "roomId": calendar_item["room_id"], + "roomTypeId": calendar_item["room_type_id"], + "id": calendar_item["id"], + "state": calendar_item["state"], + "priceDayTotal": calendar_item["price_day_total"], + "toAssign": calendar_item["to_assign"], + "splitted": calendar_item["splitted"], + "partnerId": calendar_item["partner_id"], + "partnerName": calendar_item["partner_name"], + "folioId": calendar_item["folio_id"], + "reservationId": calendar_item["reservation_id"], + "reservationName": calendar_item["reservation_name"], + "reservationType": calendar_item["reservation_type"], + "checkin": datetime.combine( + calendar_item["checkin"], datetime.min.time() + ).isoformat(), + "checkout": datetime.combine( + calendar_item["checkout"], datetime.min.time() + ).isoformat(), + "priceTotal": calendar_item["price_total"], + "adults": calendar_item["adults"], + "children": calendar_item["children"] or 0, + "pendingPayment": calendar_item["folio_pending_amount"], + "closureReasonId": calendar_item["closure_reason_id"], + "isFirstNight": calendar_item["date"] == calendar_item["checkin"] + if calendar_item["checkin"] + else None, + "isLastNight": calendar_item["date"] + == calendar_item["checkout"] + timedelta(days=-1) + if calendar_item["checkout"] + else None, + "nextLineSplitted": next_itemSplitted, + "previousLineSplitted": previous_itemSplitted, + } + + +def build_restriction(item): + result = {} + if item["closed"] is not None and item["closed"]: + result.update({"closed": True}) + if item["closed_arrival"] is not None and item["closed_arrival"]: + result.update({"closedArrival": True}) + if item["closed_departure"] is not None and item["closed_departure"]: + result.update({"closedDeparture": True}) + if item["min_stay"] is not None and item["min_stay"] != 0: + result.update({"minStay": item["min_stay"]}) + if item["max_stay"] is not None and item["max_stay"] != 0: + result.update({"maxStay": item["max_stay"]}) + if item["min_stay_arrival"] is not None and item["min_stay_arrival"] != 0: + result.update({"minStayArrival": item["min_stay_arrival"]}) + if item["max_stay_arrival"] is not None and item["max_stay_arrival"] != 0: + result.update({"maxStayArrival": item["max_stay_arrival"]}) + return result + + +class PmsCalendarService(Component): + _inherit = "base.rest.service" + _name = "pms.private.service" + _usage = "calendar" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/old-calendar", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.search.param"), + output_param=Datamodel("pms.calendar.info", is_list=True), + auth="jwt_api_pms", + ) + def get_old_reservations_calendar(self, calendar_search_param): + date_from = datetime.strptime(calendar_search_param.dateFrom, "%Y-%m-%d").date() + date_to = datetime.strptime(calendar_search_param.dateTo, "%Y-%m-%d").date() + count_nights = (date_to - date_from).days + 1 + target_dates = [date_from + timedelta(days=x) for x in range(count_nights)] + pms_property_id = calendar_search_param.pmsPropertyId + + selected_fields_mapper = { + "id": "night.id", + "state": "night.state", + "date": "DATE(night.date)", + "room_id": "night.room_id", + "room_type_name": "pms_room_type.default_code", + "to_assign": "reservation.to_assign", + "splitted": "reservation.splitted", + "partner_id": "reservation.partner_id", + "partner_name": "reservation.partner_name", + "folio_id": "folio.id", + "reservation_id": "reservation.id", + "reservation_name": "reservation.name", + "reservation_type": "reservation.reservation_type", + "checkin": "reservation.checkin", + "checkout": "reservation.checkout", + "price_total": "reservation.price_total", + "folio_pending_amount": "folio.pending_amount", + "adults": "reservation.adults", + "price_day_total": "night.price_day_total", + "closure_reason_id": "folio.closure_reason_id", + "is_reselling": "reservation.is_reselling", + # "price_day_total_services": subselect_sum_services_price, + } + selected_fields_sql = list(selected_fields_mapper.values()) + selected_fields = list(selected_fields_mapper.keys()) + sql_select = "SELECT %s" % ", ".join(selected_fields_sql) + self.env.cr.execute( + f""" + {sql_select} + FROM pms_reservation_line night + LEFT JOIN pms_reservation reservation + ON reservation.id = night.reservation_id + LEFT JOIN pms_room_type + ON pms_room_type.id = reservation.room_type_id + LEFT JOIN pms_folio folio + ON folio.id = reservation.folio_id + WHERE (night.pms_property_id = %s) + AND (night.date in %s) + AND (night.state != 'cancel') + AND (night.occupies_availability = True) + """, + ( + pms_property_id, + tuple(target_dates), + ), + ) + result_sql = self.env.cr.fetchall() + lines = [] + for res in result_sql: + lines.append( + {field: res[selected_fields.index(field)] for field in selected_fields} + ) + + PmsCalendarInfo = self.env.datamodels["pms.calendar.info"] + result_lines = [] + for line in lines: + next_line_splitted = False + previous_line_splitted = False + is_first_night = line["checkin"] == line["date"] + is_last_night = line["checkout"] + timedelta(days=-1) == line["date"] + if line.get("splitted"): + next_line = next( + ( + item + for item in lines + if item["reservation_id"] == line["reservation_id"] + and item["date"] == line["date"] + timedelta(days=1) + ), + False, + ) + if next_line: + next_line_splitted = next_line["room_id"] != line["room_id"] + + previous_line = next( + ( + item + for item in lines + if item["reservation_id"] == line["reservation_id"] + and item["date"] == line["date"] + timedelta(days=-1) + ), + False, + ) + if previous_line: + previous_line_splitted = previous_line["room_id"] != line["room_id"] + result_lines.append( + PmsCalendarInfo( + id=line["id"], + state=line["state"], + date=datetime.combine( + line["date"], datetime.min.time() + ).isoformat(), + roomId=line["room_id"], + roomTypeName=str(line["room_type_name"]), + toAssign=line["to_assign"], + splitted=line["splitted"], + partnerId=line["partner_id"] or None, + partnerName=line["partner_name"] or None, + folioId=line["folio_id"], + reservationId=line["reservation_id"], + reservationName=line["reservation_name"], + reservationType=line["reservation_type"], + isFirstNight=is_first_night, + isLastNight=is_last_night, + totalPrice=round(line["price_total"], 2), + pendingPayment=round(line["folio_pending_amount"], 2), + priceDayTotal=round(line["price_day_total"], 0), + priceDayTotalServices=0, + numNotifications=0, + adults=line["adults"], + nextLineSplitted=next_line_splitted, + previousLineSplitted=previous_line_splitted, + closureReasonId=line["closure_reason_id"], + isReselling=line["is_reselling"] if line["is_reselling"] else False, + ) + ) + return result_lines + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.search.param"), + output_param=Datamodel("pms.calendar.render.info", is_list=True), + auth="jwt_api_pms", + ) + def get_reservations_calendar(self, calendar_search_param): + response = [] + date_from = datetime.strptime(calendar_search_param.dateFrom, "%Y-%m-%d").date() + date_to = datetime.strptime(calendar_search_param.dateTo, "%Y-%m-%d").date() + selected_fields_mapper = { + "date": "dr.date date", + "room_id": "dr.room_id room_id", + "capacity": "dr.capacity capacity", + "room_type_id": "dr.room_type_id room_type_id", + "room_type_class_id": "dr.room_type_class_id room_type_class_id", + "id": "l.id id", + "state": "l.state state", + "price_day_total": "l.price_day_total price_day_total", + "to_assign": "r.to_assign to_assign", + "splitted": "r.splitted splitted", + "partner_id": "r.partner_id partner_id", + "partner_name": "r.partner_name partner_name", + "folio_id": "r.folio_id folio_id", + "reservation_id": "r.id reservation_id", + "reservation_name": "r.name reservation_name", + "reservation_type": "r.reservation_type reservation_type", + "checkin": "r.checkin checkin", + "checkout": "r.checkout checkout", + "price_total": "r.price_total price_total", + "adults": "r.adults adults", + "children": "r.children children", + "folio_pending_amount": "f.pending_amount folio_pending_amount", + "closure_reason_id": "f.closure_reason_id closure_reason_id", + "closed": "ru.closed closed", + "closed_departure": "ru.closed_departure closed_departure", + "closed_arrival": "ru.closed_arrival closed_arrival", + "min_stay": "ru.min_stay min_stay", + "min_stay_arrival": "ru.min_stay_arrival min_stay_arrival", + "max_stay": "ru.max_stay max_stay", + "max_stay_arrival": "ru.max_stay_arrival max_stay_arrival", + } + selected_fields_sql = list(selected_fields_mapper.values()) + sql_select = "SELECT %s" % ", ".join(selected_fields_sql) + self.env.cr.execute( + f""" + {sql_select} + FROM + (SELECT dates.date, + r_rt_rtc.room_id, + r_rt_rtc.capacity, + r_rt_rtc.room_type_id, + r_rt_rtc.room_type_class_id, + r_rt_rtc.sequence + FROM (SELECT (CURRENT_DATE + date ) date + FROM generate_series(date %s- CURRENT_DATE, date %s - CURRENT_DATE) date + ) dates, + (SELECT r.id room_id, r.capacity, rt.id room_type_id, + rtc.id room_type_class_id, r.sequence + FROM pms_room r + INNER JOIN pms_room_type rt ON rt.id = r.room_type_id + INNER JOIN pms_room_type_class rtc ON rtc.id = rt.class_id + WHERE r.active = true AND r.pms_property_id = %s) r_rt_rtc + ) dr + LEFT OUTER JOIN ( + SELECT l.id, l.state, l.price_day_total, + l.room_id, l.date, l.reservation_id + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON r.id = l.reservation_id + WHERE l.pms_property_id = %s AND l.state != 'cancel' + AND l.occupies_availability = true AND l.date <= %s + AND l.reservation_id = r.id + AND r.overbooking = false + ) l ON l.room_id = dr.room_id AND l.date = dr.date + LEFT OUTER JOIN (SELECT date, room_type_id, min_stay, min_stay_arrival, + max_stay, max_stay_arrival, closed, closed_departure, closed_arrival + FROM pms_availability_plan_rule + WHERE availability_plan_id = %s and pms_property_id = %s + ) ru ON ru.date = dr.date AND ru.room_type_id = dr.room_type_id + LEFT OUTER JOIN pms_reservation r ON l.reservation_id = r.id + LEFT OUTER JOIN pms_folio f ON r.folio_id = f.id + ORDER BY dr.sequence, dr.room_id, dr.date + """, + ( + calendar_search_param.dateFrom, + calendar_search_param.dateTo, + calendar_search_param.pmsPropertyId, + calendar_search_param.pmsPropertyId, + calendar_search_param.dateTo, + calendar_search_param.availabilityPlanId, + calendar_search_param.pmsPropertyId, + ), + ) + result = self.env.cr.dictfetchall() + CalendarRenderInfo = self.env.datamodels["pms.calendar.render.info"] + last_room_id = False + last_reservation_id = False + index_date_last_reservation = False + for index, item in enumerate(result): + date = { + "date": datetime.combine(item["date"], datetime.min.time()).isoformat(), + "reservationLines": [], + } + restriction = build_restriction(item) + if restriction: + date.update( + { + "restriction": restriction, + } + ) + + if last_room_id != item["room_id"]: + last_room_id = item["room_id"] + last_reservation_id = False + response.append( + CalendarRenderInfo( + roomId=item["room_id"], + capacity=item["capacity"], + roomTypeClassId=item["room_type_class_id"], + roomTypeId=item["room_type_id"], + dates=[date], + ) + ) + else: + response[-1].dates.append(date) + if ( + item["reservation_id"] is not None + and item["reservation_id"] != last_reservation_id + ): + response[-1].dates[-1]["reservationLines"].append( + build_reservation_line_info( + item, + previous_item=False + if (not item["splitted"] or item["date"] == date_from) + else result[index - 1], + next_item=False + if (not item["splitted"] or item["date"] == date_to) + else result[index + 1], + ) + ) + last_reservation_id = item["reservation_id"] + index_date_last_reservation = len(response[-1].dates) - 1 + elif ( + item["reservation_id"] is not None + and item["reservation_id"] == last_reservation_id + ): + response[-1].dates[index_date_last_reservation][ + "reservationLines" + ].append( + build_reservation_line_info( + item, + previous_item=False + if (not item["splitted"] or item["date"] == date_from) + else result[index - 1], + next_item=False + if (not item["splitted"] or item["date"] == date_to) + else result[index + 1], + ) + ) + last_reservation_id = item["reservation_id"] + else: + last_reservation_id = False + return response + + @restapi.method( + [ + ( + [ + "/calendar-prices-rules", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.search.param"), + output_param=Datamodel("pms.calendar.prices.rules.render.info", is_list=True), + auth="jwt_api_pms", + ) + def get_prices_rules_calendar(self, calendar_search_param): + response = [] + date_from = datetime.strptime(calendar_search_param.dateFrom, "%Y-%m-%d").date() + date_to = datetime.strptime(calendar_search_param.dateTo, "%Y-%m-%d").date() + + self.env.cr.execute( + """ + SELECT dr.room_type_id, + dr.date date, + it.id pricelist_item_id, + av.id availability_plan_rule_id, + COALESCE(av.max_avail, dr.default_max_avail) max_avail, + COALESCE(av.quota, dr.default_quota) quota, + COALESCE(av.closed, FALSE) closed, + COALESCE(av.closed_arrival, FALSE) closed_arrival, + COALESCE(av.closed_Departure, FALSE) closed_departure, + COALESCE(av.min_stay, 0) min_stay, + COALESCE(av.min_stay_arrival, 0) min_stay_arrival, + COALESCE(av.max_stay, 0) max_stay, + COALESCE(av.max_stay_arrival, 0) max_stay_arrival, + COALESCE(it.fixed_price, ( + SELECT ipp.value_float + FROM ir_pms_property ipp, (SELECT id field_id, model_id + FROM ir_model_fields + WHERE name = 'list_price' + AND model = 'product.template' + ) imf + WHERE ipp.model_id = imf.model_id + AND ipp.field_id = imf.field_id + AND ipp.record = pp.product_tmpl_id + AND ipp.pms_property_id = %s + ) + ) price, + (SELECT COUNT (1) + FROM pms_room r + WHERE r.room_type_id = dr.room_type_id + AND r.active = true AND r.pms_property_id = %s + AND NOT EXISTS (SELECT 1 + FROM pms_reservation_line + WHERE date = dr.date + AND occupies_availability = true + AND room_id = r.id + AND r.is_shared_room = false) + ) free_rooms + FROM + ( + SELECT dates.date, rt_r.room_type_id, rt_r.sequence, + rt_r.product_id, rt_r.default_max_avail, rt_r.default_quota + FROM + ( + SELECT (CURRENT_DATE + date) date + FROM generate_series(date %s- CURRENT_DATE, date %s - CURRENT_DATE) date + ) dates, + ( + SELECT rt.id room_type_id, + rt.sequence, + rt.product_id, + rt.default_max_avail, + rt.default_quota + FROM pms_room_type rt + WHERE EXISTS ( SELECT 1 + FROM pms_room + WHERE pms_property_id = %s + AND room_type_id = rt.id + AND active = true) + ) rt_r + ) dr + INNER JOIN product_product pp ON pp.id = dr.product_id + LEFT OUTER JOIN pms_availability_plan_rule av ON av.date = dr.date + AND av.room_type_id = dr.room_type_id + AND av.pms_property_id = %s + AND av.availability_plan_id = %s + LEFT OUTER JOIN product_pricelist_item it ON it.date_start_consumption = dr.date + AND it.date_end_consumption = dr.date + AND it.product_id = dr.product_id + AND it.active = true + AND it.pricelist_id = %s + AND EXISTS (SELECT 1 + FROM product_pricelist_item_pms_property_rel relp + WHERE relp.product_pricelist_item_id = it.id + AND relp.pms_property_id = %s) + ORDER BY dr.sequence, dr.room_type_id, dr.date; + """, + ( + calendar_search_param.pmsPropertyId, + calendar_search_param.pmsPropertyId, + date_from, + date_to, + calendar_search_param.pmsPropertyId, + calendar_search_param.pmsPropertyId, + calendar_search_param.availabilityPlanId, + calendar_search_param.pricelistId, + calendar_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + CalendarPricesRulesRenderInfo = self.env.datamodels[ + "pms.calendar.prices.rules.render.info" + ] + last_room_type_id = False + for _index, item in enumerate(result): + date = { + "date": datetime.combine(item["date"], datetime.min.time()).isoformat(), + "freeRooms": item["free_rooms"], + "pricelistItemId": item["pricelist_item_id"], + "price": item["price"], + # + "availabilityPlanRuleId": item["availability_plan_rule_id"], + "maxAvail": item["max_avail"], + "quota": item["quota"], + "closed": item["closed"], + "closedArrival": item["closed_arrival"], + "closedDeparture": item["closed_departure"], + "minStay": item["min_stay"], + "minStayArrival": item["min_stay_arrival"], + "maxStay": item["max_stay"], + "maxStayArrival": item["max_stay_arrival"], + } + if last_room_type_id != item["room_type_id"]: + last_room_type_id = item["room_type_id"] + response.append( + CalendarPricesRulesRenderInfo( + roomTypeId=item["room_type_id"], + dates=[date], + ) + ) + else: + response[-1].dates.append(date) + return response + + @restapi.method( + [ + ( + [ + "/calendar-headers", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.header.search.param"), + output_param=Datamodel("pms.calendar.header.info", is_list=True), + auth="jwt_api_pms", + ) + def get_calendar_headers(self, calendar_search_param): + response = [] + date_from = datetime.strptime(calendar_search_param.dateFrom, "%Y-%m-%d").date() + date_to = datetime.strptime(calendar_search_param.dateTo, "%Y-%m-%d").date() + + room_ids = tuple(calendar_search_param.roomIds) + + self.env.cr.execute( + """ + SELECT d.date, + bool_or(l.overbooking) overbooking, + CEIL(SUM(l.price_day_total)) daily_billing, + tr.num_total_rooms + - + ( + SELECT COUNT(1) + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON r.id = l.reservation_id + WHERE l.date = d.date + AND l.pms_property_id = %s + AND l.state != 'cancel' + AND l.occupies_availability = true + AND l.room_id IN %s + AND r.overbooking = false + ) free_rooms, + CEIL(( + SELECT COUNT(1) + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON r.id = l.reservation_id + WHERE r.reservation_type NOT IN ('out', 'staff') + AND r.pms_property_id = %s + AND l.occupies_availability = true + AND l.state != 'cancel' + AND l.room_id IN %s + AND l.date = d.date + AND r.overbooking = false + ) * 100.00 / tr.num_total_rooms) occupancy_rate + FROM ( + SELECT (CURRENT_DATE + date) date + FROM generate_series(date %s- CURRENT_DATE, date %s - CURRENT_DATE + ) date) d + LEFT OUTER JOIN ( SELECT date, price_day_total, overbooking + FROM pms_reservation_line + WHERE pms_property_id = %s + AND room_id IN %s + ) l ON l.date = d.date, + ( SELECT COUNT(1) num_total_rooms + FROM pms_room + WHERE pms_property_id = %s + AND id IN %s + ) tr + GROUP BY d.date, tr.num_total_rooms + ORDER BY d.date; + """, + ( + calendar_search_param.pmsPropertyId, + room_ids, + calendar_search_param.pmsPropertyId, + room_ids, + date_from, + date_to, + calendar_search_param.pmsPropertyId, + room_ids, + calendar_search_param.pmsPropertyId, + room_ids, + ), + ) + + result = self.env.cr.dictfetchall() + CalendarHeaderInfo = self.env.datamodels["pms.calendar.header.info"] + + for item in result: + response.append( + CalendarHeaderInfo( + date=datetime.combine( + item["date"], datetime.min.time() + ).isoformat(), + dailyBilling=item["daily_billing"] if item["daily_billing"] else 0, + freeRooms=item["free_rooms"] if item["free_rooms"] else 0, + occupancyRate=item["occupancy_rate"] + if item["occupancy_rate"] + else 0, + overbooking=item["overbooking"] if item["overbooking"] else False, + ) + ) + + return response + + @restapi.method( + [ + ( + [ + "/swap", + ], + "POST", + ) + ], + input_param=Datamodel("pms.calendar.swap.info", is_list=False), + auth="jwt_api_pms", + ) + def swap_reservation_slices(self, swap_info): + reservation_lines_target = ( + self.env["pms.reservation.line"] + .search([("id", "in", swap_info.reservationLineIds)]) + .sorted(key=lambda l: l.date) + ) + + for reservation_line in reservation_lines_target: + old_room_id = reservation_line.room_id + affected_line = self.env["pms.reservation.line"].search( + [ + ("date", "=", reservation_line.date), + ("room_id", "=", swap_info.roomId), + ] + ) + reservation_line.with_context( + avoid_availability_check=True + ).room_id = swap_info.roomId + affected_line.with_context( + avoid_availability_check=True + ).room_id = old_room_id + + @restapi.method( + [ + ( + [ + "/daily-invoicing", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.search.param", is_list=False), + output_param=Datamodel("pms.calendar.daily.invoicing", is_list=True), + auth="jwt_api_pms", + ) + def get_daily_invoincing(self, pms_calendar_search_param): + date_from = datetime.strptime( + pms_calendar_search_param.dateFrom, "%Y-%m-%d" + ).date() + date_to = datetime.strptime(pms_calendar_search_param.dateTo, "%Y-%m-%d").date() + count_nights = (date_to - date_from).days + 1 + target_dates = [date_from + timedelta(days=x) for x in range(count_nights)] + pms_property_id = pms_calendar_search_param.pmsPropertyId + + self.env.cr.execute( + """ + SELECT night.date, SUM(night.price_day_total) AS production + FROM pms_reservation_line night + WHERE (night.pms_property_id = %s) + AND (night.date in %s) + GROUP BY night.date + """, + ( + pms_property_id, + tuple(target_dates), + ), + ) + production_per_nights_date = self.env.cr.fetchall() + + self.env.cr.execute( + """ + SELECT service.date, SUM(service.price_day_total) AS production + FROM pms_service_line service + WHERE (service.pms_property_id = %s) + AND (service.date in %s) + GROUP BY service.date + """, + ( + pms_property_id, + tuple(target_dates), + ), + ) + production_per_services_date = self.env.cr.fetchall() + + production_per_nights_dict = [ + {"date": item[0], "total": item[1]} for item in production_per_nights_date + ] + production_per_services_dict = [ + {"date": item[0], "total": item[1]} for item in production_per_services_date + ] + + result = [] + PmsCalendarDailyInvoicing = self.env.datamodels["pms.calendar.daily.invoicing"] + for day in target_dates: + night_production = next( + ( + item["total"] + for item in production_per_nights_dict + if item["date"] == day + ), + False, + ) + service_production = next( + ( + item["total"] + for item in production_per_services_dict + if item["date"] == day + ), + False, + ) + result.append( + PmsCalendarDailyInvoicing( + date=datetime.combine(day, datetime.min.time()).isoformat(), + invoicingTotal=round( + (night_production or 0) + (service_production or 0), 2 + ), + ) + ) + + return result + + @restapi.method( + [ + ( + [ + "/free-rooms", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.search.param", is_list=False), + output_param=Datamodel("pms.calendar.free.daily.rooms.by.type", is_list=True), + auth="jwt_api_pms", + ) + def get_free_rooms(self, pms_calendar_search_param): + date_from = datetime.strptime( + pms_calendar_search_param.dateFrom, "%Y-%m-%d" + ).date() + date_to = datetime.strptime(pms_calendar_search_param.dateTo, "%Y-%m-%d").date() + count_nights = (date_to - date_from).days + 1 + target_dates = [date_from + timedelta(days=x) for x in range(count_nights)] + pms_property_id = pms_calendar_search_param.pmsPropertyId + + self.env.cr.execute( + """ + SELECT night.date AS date, room.room_type_id AS room_type, COUNT(night.id) AS count + FROM pms_reservation_line night + LEFT JOIN pms_room room + ON night.room_id = room.id + WHERE (night.pms_property_id = %s) + AND (night.date in %s) + AND (night.occupies_availability = True) + GROUP BY night.date, room.room_type_id + """, + ( + pms_property_id, + tuple(target_dates), + ), + ) + result_sql = self.env.cr.fetchall() + rooms = self.env["pms.room"].search([("pms_property_id", "=", pms_property_id)]) + room_types = rooms.mapped("room_type_id") + total_rooms_by_room_type = [ + { + "room_type_id": room_type.id, + "rooms_total": len( + self.env["pms.room"] + .with_context(active_test=True) + .search( + [ + ("room_type_id", "=", room_type.id), + ("pms_property_id", "=", pms_property_id), + ] + ) + ), + } + for room_type in room_types + ] + PmsCalendarFreeDailyRoomsByType = self.env.datamodels[ + "pms.calendar.free.daily.rooms.by.type" + ] + result = [] + for day in target_dates: + for total_room_type in total_rooms_by_room_type: + count_occupied_night_by_room_type = next( + ( + item[2] + for item in result_sql + if item[0] == day and item[1] == total_room_type["room_type_id"] + ), + 0, + ) + result.append( + PmsCalendarFreeDailyRoomsByType( + date=str( + datetime.combine(day, datetime.min.time()).isoformat() + ), + roomTypeId=total_room_type["room_type_id"], + freeRooms=total_room_type["rooms_total"] + - count_occupied_night_by_room_type, + ) + ) + return result + + @restapi.method( + [ + ( + [ + "/alerts-per-day", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.search.param", is_list=False), + output_param=Datamodel("pms.calendar.alerts.per.day", is_list=True), + auth="jwt_api_pms", + ) + def get_alerts_per_day(self, pms_calendar_search_param): + date_from = datetime.strptime( + pms_calendar_search_param.dateFrom, "%Y-%m-%d" + ).date() + date_to = datetime.strptime(pms_calendar_search_param.dateTo, "%Y-%m-%d").date() + count_nights = (date_to - date_from).days + 1 + target_dates = [date_from + timedelta(days=x) for x in range(count_nights)] + pms_property_id = pms_calendar_search_param.pmsPropertyId + + self.env.cr.execute( + """ + SELECT night.date AS date, COUNT(night.id) AS count + FROM pms_reservation_line night + WHERE (night.pms_property_id = %s) + AND (night.date in %s) + AND (night.overbooking = True) + GROUP BY night.date + """, + ( + pms_property_id, + tuple(target_dates), + ), + ) + result_sql = self.env.cr.fetchall() + PmsCalendarAlertsPerDay = self.env.datamodels["pms.calendar.alerts.per.day"] + result = [] + for day in target_dates: + overbooking_lines = next( + (item[1] for item in result_sql if item[0] == day), 0 + ) + result.append( + PmsCalendarAlertsPerDay( + date=str(datetime.combine(day, datetime.min.time()).isoformat()), + overbooking=True if overbooking_lines > 0 else False, + ) + ) + return result + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.reservation.updates", is_list=False), + auth="jwt_api_pms", + ) + def update_reservation(self, reservation_id, reservation_lines_changes): + if reservation_lines_changes.reservationLinesChanges: + + # TEMP: Disabled temporal date changes to avoid drag&drops errors + lines_to_change = self.env["pms.reservation.line"].browse( + [ + item["reservationLineId"] + for item in reservation_lines_changes.reservationLinesChanges + ] + ) + lines_to_change.room_id = reservation_lines_changes.reservationLinesChanges[ + 0 + ]["roomId"] + # # get date of first reservation id to change + # first_reservation_line_id_to_change = ( + # reservation_lines_changes.reservationLinesChanges[0][ + # "reservationLineId" + # ] + # ) + # first_reservation_line_to_change = self.env["pms.reservation.line"].browse( + # first_reservation_line_id_to_change + # ) + # date_first_reservation_line_to_change = datetime.strptime( + # reservation_lines_changes.reservationLinesChanges[0]["date"], "%Y-%m-%d" + # ) + + # # iterate changes + # for change_iterator in sorted( + # reservation_lines_changes.reservationLinesChanges, + # # adjust order to start changing from last/first reservation line + # # to avoid reservation line date constraint + # reverse=first_reservation_line_to_change.date + # < date_first_reservation_line_to_change.date(), + # key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"), + # ): + # # recordset of each line + # line_to_change = self.env["pms.reservation.line"].search( + # [ + # ("reservation_id", "=", reservation_id), + # ("id", "=", change_iterator["reservationLineId"]), + # ] + # ) + # # modifying date, room_id, ... + # if "date" in change_iterator: + # line_to_change.date = change_iterator["date"] + # if ( + # "roomId" in change_iterator + # and line_to_change.room_id.id != change_iterator["roomId"] + # ): + # line_to_change.room_id = change_iterator["roomId"] + + # max_value = max( + # first_reservation_line_to_change.reservation_id.reservation_line_ids.mapped( + # "date" + # ) + # ) + timedelta(days=1) + # min_value = min( + # first_reservation_line_to_change.reservation_id.reservation_line_ids.mapped( + # "date" + # ) + # ) + # reservation = self.env["pms.reservation"].browse(reservation_id) + # reservation.checkin = min_value + # reservation.checkout = max_value + + else: + reservation_to_update = ( + self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) + ) + reservation_vals = {} + + if reservation_lines_changes.preferredRoomId: + reservation_vals.update( + {"preferred_room_id": reservation_lines_changes.preferredRoomId} + ) + if reservation_lines_changes.boardServiceId is not None: + reservation_vals.update( + {"board_service_room_id": reservation_lines_changes.boardServiceId} + ) + if reservation_lines_changes.pricelistId: + reservation_vals.update( + {"pricelist_id": reservation_lines_changes.pricelistId} + ) + if reservation_lines_changes.adults: + reservation_vals.update({"adults": reservation_lines_changes.adults}) + if reservation_lines_changes.children is not None: + reservation_vals.update( + {"children": reservation_lines_changes.children} + ) + if reservation_lines_changes.segmentationId: + reservation_vals.update( + { + "segmentation_ids": [ + (6, 0, [reservation_lines_changes.segmentationId]) + ] + } + ) + reservation_to_update.write(reservation_vals) diff --git a/pms_api_rest/services/pms_cancelation_rule_service.py b/pms_api_rest/services/pms_cancelation_rule_service.py new file mode 100644 index 0000000000..d0616f0325 --- /dev/null +++ b/pms_api_rest/services/pms_cancelation_rule_service.py @@ -0,0 +1,83 @@ +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsCancelationRuleService(Component): + _inherit = "base.rest.service" + _name = "pms.cancelation.rule.service" + _usage = "cancelation-rules" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.cancelation.rule.search.param"), + output_param=Datamodel("pms.cancelation.rule.info", is_list=True), + auth="jwt_api_pms", + ) + def get_cancelation_rules(self, cancelation_rule_search_param): + domain = [] + + if cancelation_rule_search_param.pricelistId: + domain.append( + ("pricelist_ids", "in", [cancelation_rule_search_param.pricelistId]) + ) + if cancelation_rule_search_param.pmsPropertyId: + domain.extend( + [ + "|", + ( + "pms_property_ids", + "in", + [cancelation_rule_search_param.pmsPropertyId], + ), + ("pms_property_ids", "=", False), + ] + ) + result_cancelation_rules = [] + PmsCancelationRuleInfo = self.env.datamodels["pms.cancelation.rule.info"] + for cancelation_rule in self.env["pms.cancelation.rule"].search( + domain, + ): + result_cancelation_rules.append( + PmsCancelationRuleInfo( + id=cancelation_rule.id, + name=cancelation_rule.name, + ) + ) + return result_cancelation_rules + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.cancelation.rule.info", is_list=False), + auth="jwt_api_pms", + ) + def get_cancelation_rule(self, cancelation_rule_id): + cancelation_rule = self.env["pms.cancelation.rule"].search( + [("id", "=", cancelation_rule_id)] + ) + if cancelation_rule: + PmsCancelationRuleInfo = self.env.datamodels["pms.cancelation.rule.info"] + return PmsCancelationRuleInfo( + id=cancelation_rule.id, + name=cancelation_rule.name, + ) + else: + raise MissingError(_("Cancelation Rule not found")) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py new file mode 100644 index 0000000000..92738956b2 --- /dev/null +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -0,0 +1,742 @@ +from datetime import datetime + +from odoo import fields + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsDashboardServices(Component): + _inherit = "base.rest.service" + _name = "pms.dashboard.service" + _usage = "dashboard" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/pending-reservations", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.range.dates.search.param"), + output_param=Datamodel("pms.dashboard.pending.reservations", is_list=True), + auth="jwt_api_pms", + ) + def get_pending_reservations(self, pms_dashboard_search_param): + dateFrom = fields.Date.from_string(pms_dashboard_search_param.dateFrom) + dateTo = fields.Date.from_string(pms_dashboard_search_param.dateTo) + + # If you modify this SQL you must modify the get_folios service in pms_folio_service.py + self.env.cr.execute( + """ + SELECT + d.date, + SUM(CASE WHEN r.checkin = d.date AND r.state IN ('confirm', 'arrival_delayed') + THEN 1 ELSE 0 + END) AS reservations_pending_arrival, + SUM(CASE WHEN r.checkin = d.date AND r.state = 'onboard' THEN 1 ELSE 0 + END) AS + reservations_on_board, + SUM(CASE WHEN r.checkout = d.date AND r.state IN ('onboard', 'departure_delayed') + THEN 1 ELSE 0 + END) AS reservations_pending_departure, + SUM(CASE WHEN r.checkout = d.date AND r.state = 'done' THEN 1 ELSE 0 END) + AS reservations_completed + FROM ( SELECT CURRENT_DATE + date AS date + FROM generate_series(date %s - CURRENT_DATE, date %s - CURRENT_DATE) date) d + LEFT JOIN pms_reservation r + ON (r.checkin = d.date OR r.checkout = d.date) + AND r.pms_property_id = %s + AND r.reservation_type != 'out' + GROUP BY d.date + ORDER BY d.date; + """, + ( + dateFrom, + dateTo, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + pending_reservations = [] + DashboardPendingReservations = self.env.datamodels[ + "pms.dashboard.pending.reservations" + ] + + for item in result: + pending_reservations.append( + DashboardPendingReservations( + date=datetime.combine( + item["date"], datetime.min.time() + ).isoformat(), + pendingArrivalReservations=item["reservations_pending_arrival"] + if item["reservations_pending_arrival"] + else 0, + completedArrivalReservations=item["reservations_on_board"] + if item["reservations_on_board"] + else 0, + pendingDepartureReservations=item["reservations_pending_departure"] + if item["reservations_pending_departure"] + else 0, + completedDepartureReservations=item["reservations_completed"] + if item["reservations_completed"] + else 0, + ) + ) + return pending_reservations + + @restapi.method( + [ + ( + [ + "/occupancy", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.search.param"), + output_param=Datamodel("pms.dashboard.numeric.response"), + auth="jwt_api_pms", + ) + def get_occupancy(self, pms_dashboard_search_param): + date_occupancy = fields.Date.from_string(pms_dashboard_search_param.date) + + self.env.cr.execute( + """ + SELECT CEIL(l.num * 100.00 / tr.num_total_rooms) AS occupancy + FROM + ( + SELECT COUNT(1) num_total_rooms + FROM pms_room + WHERE pms_property_id = %s + ) tr, + ( + SELECT COUNT(1) num + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON r.id = l.reservation_id + WHERE r.reservation_type NOT IN ('out', 'staff') + AND l.occupies_availability = true + AND l.state != 'cancel' + AND l.date = %s + AND r.pms_property_id = %s + ) l + """, + ( + pms_dashboard_search_param.pmsPropertyId, + date_occupancy, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + + return DashboardNumericResponse( + value=result[0]["occupancy"] if result[0]["occupancy"] else 0, + ) + + @restapi.method( + [ + ( + [ + "/state-rooms", + ], + "GET", + ) + ], + auth="jwt_api_pms", + input_param=Datamodel("pms.dashboard.range.dates.search.param"), + output_param=Datamodel("pms.dashboard.state.rooms", is_list=True), + ) + def get_state_rooms(self, pms_dashboard_search_param): + dateFrom = fields.Date.from_string(pms_dashboard_search_param.dateFrom) + dateTo = fields.Date.from_string(pms_dashboard_search_param.dateTo) + self.env.cr.execute( + """ + SELECT d.date, + COALESCE(rln.num_occupied_rooms, 0) num_occupied_rooms, + COALESCE( rlo.num_out_of_service_rooms, 0) num_out_of_service_rooms, + COALESCE(total_rooms.num_total_rooms, 0) + - COALESCE(rln.num_occupied_rooms, 0) + - COALESCE( rlo.num_out_of_service_rooms, 0) free_rooms + FROM + ( + SELECT (CURRENT_DATE + date) date + FROM generate_series(date %s - CURRENT_DATE, date %s - CURRENT_DATE + ) date) d + LEFT OUTER JOIN (SELECT COUNT(1) num_occupied_rooms, date + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON l.reservation_id = r.id + WHERE l.pms_property_id = %s + AND l.occupies_availability + AND r.reservation_type != 'out' + GROUP BY date + ) rln ON rln.date = d.date + LEFT OUTER JOIN (SELECT COUNT(1) num_out_of_service_rooms, date + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON l.reservation_id = r.id + WHERE l.pms_property_id = %s + AND l.occupies_availability + AND r.reservation_type = 'out' + GROUP BY date + ) rlo ON rlo.date = d.date + LEFT OUTER JOIN (SELECT COUNT(1) num_total_rooms + FROM pms_room + WHERE pms_property_id = %s + ) total_rooms ON true + GROUP BY d.date, num_occupied_rooms, num_out_of_service_rooms, num_total_rooms + ORDER BY d.date + """, + ( + dateFrom, + dateTo, + pms_dashboard_search_param.pmsPropertyId, + pms_dashboard_search_param.pmsPropertyId, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + state_rooms_result = [] + DashboardStateRooms = self.env.datamodels["pms.dashboard.state.rooms"] + for item in result: + state_rooms_result.append( + DashboardStateRooms( + date=datetime.combine( + item["date"], datetime.min.time() + ).isoformat(), + numOccupiedRooms=item["num_occupied_rooms"] + if item["num_occupied_rooms"] + else 0, + numOutOfServiceRooms=item["num_out_of_service_rooms"] + if item["num_out_of_service_rooms"] + else 0, + numFreeRooms=item["free_rooms"] if item["free_rooms"] else 0, + ) + ) + return state_rooms_result + + @restapi.method( + [ + ( + [ + "/reservations-by-sale-channel", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.range.dates.search.param"), + output_param=Datamodel("pms.dashboard.state.rooms", is_list=True), + auth="jwt_api_pms", + ) + def get_reservations_by_sale_channel(self, pms_dashboard_search_param): + dateFrom = fields.Date.from_string(pms_dashboard_search_param.dateFrom) + dateTo = fields.Date.from_string(pms_dashboard_search_param.dateTo) + self.env.cr.execute( + """ + SELECT CASE WHEN sc.channel_type = 'direct' THEN sc.name + ELSE (SELECT name FROM res_partner WHERE id = r.agency_id) + END AS sale_channel_name, + CEIL(COUNT(r.id) * 100.00 / tr.num_total_reservations) + AS percentage_by_sale_channel + FROM + ( + SELECT COUNT(1) num_total_reservations + FROM pms_reservation + WHERE create_date::date BETWEEN %s AND %s + AND reservation_type != 'out' + AND pms_property_id = %s + ) tr, + pms_reservation r + INNER JOIN pms_sale_channel sc ON r.sale_channel_origin_id = sc.id + WHERE r.create_date::date BETWEEN %s AND %s + AND r.reservation_type != 'out' + AND r.pms_property_id = %s + GROUP BY + r.sale_channel_origin_id, + sc.channel_type, sc.name, + r.agency_id, + tr.num_total_reservations + ORDER BY percentage_by_sale_channel DESC; + """, + ( + dateFrom, + dateTo, + pms_dashboard_search_param.pmsPropertyId, + dateFrom, + dateTo, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + state_rooms_result = [] + DashboardReservationsBySaleChannel = self.env.datamodels[ + "pms.dashboard.reservations.by.sale.channel" + ] + for item in result: + state_rooms_result.append( + DashboardReservationsBySaleChannel( + saleChannelName=item["sale_channel_name"] + if item["sale_channel_name"] + else "", + percentageReservationsSoldBySaleChannel=item[ + "percentage_by_sale_channel" + ] + if item["percentage_by_sale_channel"] + else 0, + ) + ) + return state_rooms_result + + @restapi.method( + [ + ( + [ + "/billing", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.search.param"), + output_param=Datamodel("pms.dashboard.numeric.response"), + auth="jwt_api_pms", + ) + def get_billing(self, pms_dashboard_search_param): + date_billing = fields.Date.from_string(pms_dashboard_search_param.date) + + self.env.cr.execute( + """ + SELECT SUM(l.price_day_total) billing + FROM pms_reservation_line l INNER JOIN pms_reservation r ON l.reservation_id = r.id + WHERE l.date = %s + AND l.occupies_availability = true + AND l.state != 'cancel' + AND l.pms_property_id = %s + AND r.reservation_type NOT IN ('out', 'staff') + """, + ( + date_billing, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + return DashboardNumericResponse( + value=result[0]["billing"] if result[0]["billing"] else 0, + ) + + @restapi.method( + [ + ( + [ + "/adr", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.range.dates.search.param"), + output_param=Datamodel("pms.dashboard.numeric.response"), + auth="jwt_api_pms", + ) + def get_adr(self, pms_dashboard_search_param): + date_from = fields.Date.from_string(pms_dashboard_search_param.dateFrom) + date_to = fields.Date.from_string(pms_dashboard_search_param.dateTo) + + pms_property = self.env["pms.property"].search( + [("id", "=", pms_dashboard_search_param.pmsPropertyId)] + ) + + adr = pms_property._get_adr(date_from, date_to) + + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + + return DashboardNumericResponse( + value=adr, + ) + + @restapi.method( + [ + ( + [ + "/revpar", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.range.dates.search.param"), + output_param=Datamodel("pms.dashboard.numeric.response"), + auth="jwt_api_pms", + ) + def get_revpar(self, pms_dashboard_search_param): + date_from = fields.Date.from_string(pms_dashboard_search_param.dateFrom) + date_to = fields.Date.from_string(pms_dashboard_search_param.dateTo) + + pms_property = self.env["pms.property"].search( + [("id", "=", pms_dashboard_search_param.pmsPropertyId)] + ) + + revpar = pms_property._get_revpar(date_from, date_to) + + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + + return DashboardNumericResponse( + value=revpar, + ) + + @restapi.method( + [ + ( + [ + "/new-folios", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.search.param"), + output_param=Datamodel("pms.dashboard.numeric.response"), + auth="jwt_api_pms", + ) + def get_number_of_new_folios(self, pms_dashboard_search_param): + date_new_folios = fields.Date.from_string(pms_dashboard_search_param.date) + + self.env.cr.execute( + """ + SELECT COUNT(1) new_folios + FROM pms_folio f + WHERE DATE(f.create_date) = %s + AND f.state != 'cancel' + AND f.pms_property_id = %s + AND f.reservation_type NOT IN ('out', 'staff') + """, + ( + date_new_folios, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + + return DashboardNumericResponse( + value=result[0]["new_folios"] if result[0]["new_folios"] else 0, + ) + + @restapi.method( + [ + ( + [ + "/overnights", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.search.param"), + output_param=Datamodel("pms.dashboard.numeric.response"), + auth="jwt_api_pms", + ) + def get_overnights(self, pms_dashboard_search_param): + date = fields.Date.from_string(pms_dashboard_search_param.date) + + self.env.cr.execute( + """ + SELECT COUNT(1) overnights + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON r.id = l.reservation_id + WHERE l.date = %s + AND l.state != 'cancel' + AND l.occupies_availability = true + AND l.pms_property_id = %s + AND l.overbooking = false + AND r.reservation_type != 'out' + """, + ( + date, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + + return DashboardNumericResponse( + value=result[0]["overnights"] if result[0]["overnights"] else 0, + ) + + @restapi.method( + [ + ( + [ + "/cancelled-overnights", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.search.param"), + output_param=Datamodel("pms.dashboard.numeric.response"), + auth="jwt_api_pms", + ) + def get_cancelled_overnights(self, pms_dashboard_search_param): + date = fields.Date.from_string(pms_dashboard_search_param.date) + + self.env.cr.execute( + """ + SELECT COUNT(1) cancelled_overnights + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON r.id = l.reservation_id + WHERE l.date = %s + AND l.state = 'cancel' + AND l.occupies_availability = false + AND l.pms_property_id = %s + AND l.overbooking = false + AND r.reservation_type != 'out' + """, + ( + date, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + + return DashboardNumericResponse( + value=result[0]["cancelled_overnights"] + if result[0]["cancelled_overnights"] + else 0, + ) + + @restapi.method( + [ + ( + [ + "/overbookings", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.search.param"), + output_param=Datamodel("pms.dashboard.numeric.response"), + auth="jwt_api_pms", + ) + def get_overbookings(self, pms_dashboard_search_param): + date = fields.Date.from_string(pms_dashboard_search_param.date) + + self.env.cr.execute( + """ + SELECT COUNT(1) overbookings + FROM pms_reservation_line l + WHERE l.date = %s + AND l.pms_property_id = %s + AND l.overbooking = true + """, + ( + date, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + + return DashboardNumericResponse( + value=result[0]["overbookings"] if result[0]["overbookings"] else 0, + ) + + @restapi.method( + [ + ( + [ + "/occupied-rooms", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.range.dates.search.param"), + output_param=Datamodel("pms.dashboard.state.rooms", is_list=True), + auth="jwt_api_pms", + ) + def get_occupied_rooms(self, pms_dashboard_search_param): + dateFrom = fields.Date.from_string(pms_dashboard_search_param.dateFrom) + dateTo = fields.Date.from_string(pms_dashboard_search_param.dateTo) + self.env.cr.execute( + """ + SELECT d.date, COALESCE(rln.num_occupied_rooms, 0) AS num_occupied_rooms + FROM + ( + SELECT (CURRENT_DATE + date) date + FROM generate_series(date %s- CURRENT_DATE, date %s - CURRENT_DATE + ) date) d + LEFT OUTER JOIN (SELECT COUNT(1) num_occupied_rooms, date + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON l.reservation_id = r.id + WHERE l.pms_property_id = %s + AND l.occupies_availability + AND r.reservation_type != 'out' + GROUP BY date + ) rln ON rln.date = d.date + GROUP BY d.date, num_occupied_rooms + ORDER BY d.date + """, + ( + dateFrom, + dateTo, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + occupied_rooms_result = [] + DashboardStateRooms = self.env.datamodels["pms.dashboard.state.rooms"] + for item in result: + occupied_rooms_result.append( + DashboardStateRooms( + date=datetime.combine( + item["date"], datetime.min.time() + ).isoformat(), + numOccupiedRooms=item["num_occupied_rooms"] + if item["num_occupied_rooms"] + else 0, + ) + ) + return occupied_rooms_result + + @restapi.method( + [ + ( + [ + "/daily-billings", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.range.dates.search.param"), + output_param=Datamodel("pms.dashboard.state.rooms", is_list=True), + auth="jwt_api_pms", + ) + def get_daily_billings(self, pms_dashboard_search_param): + dateFrom = fields.Date.from_string(pms_dashboard_search_param.dateFrom) + dateTo = fields.Date.from_string(pms_dashboard_search_param.dateTo) + self.env.cr.execute( + """ + SELECT d.date, COALESCE(rln.daily_billing, 0) AS daily_billing + FROM + ( + SELECT (CURRENT_DATE + date) date + FROM generate_series(date %s - CURRENT_DATE, date %s - CURRENT_DATE + ) date) d + LEFT OUTER JOIN (SELECT sum(l.price_day_total) daily_billing, date + FROM pms_reservation_line l + INNER JOIN pms_reservation r ON l.reservation_id = r.id + WHERE l.pms_property_id = %s + AND l.occupies_availability + AND r.reservation_type != 'out' + GROUP BY date + ) rln ON rln.date = d.date + GROUP BY d.date, daily_billing + ORDER BY d.date; + """, + ( + dateFrom, + dateTo, + pms_dashboard_search_param.pmsPropertyId, + ), + ) + + result = self.env.cr.dictfetchall() + result_daily_billings = [] + DashboardStateRooms = self.env.datamodels["pms.dashboard.daily.billing"] + for item in result: + result_daily_billings.append( + DashboardStateRooms( + date=datetime.combine( + item["date"], datetime.min.time() + ).isoformat(), + billing=item["daily_billing"] if item["daily_billing"] else 0, + ) + ) + return result_daily_billings + + @restapi.method( + [ + ( + [ + "/last-received-folios", + ], + "GET", + ), + ], + input_param=Datamodel("pms.folio.search.param", is_list=False), + output_param=Datamodel("pms.folio.short.info", is_list=True), + auth="jwt_api_pms", + ) + def get_last_received_folios(self, pms_folio_search_param): + result_folios = [] + PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] + for folio in self.env["pms.folio"].search( + [ + ("first_checkin", ">=", datetime.now().date()), + ("pms_property_id", "=", pms_folio_search_param.pmsPropertyId), + ("reservation_type", "=", "normal"), + ], + limit=pms_folio_search_param.limit, + offset=pms_folio_search_param.offset, + order="create_date desc", + ): + result_folios.append( + PmsFolioShortInfo( + id=folio.id, + name=folio.name, + state=folio.state, + partnerName=folio.partner_name if folio.partner_name else None, + partnerPhone=folio.mobile if folio.mobile else None, + partnerEmail=folio.email if folio.email else None, + amountTotal=round(folio.amount_total, 2), + pendingAmount=round(folio.pending_amount, 2), + paymentStateCode=folio.payment_state, + paymentStateDescription=dict( + folio.fields_get(["payment_state"])["payment_state"][ + "selection" + ] + )[folio.payment_state], + numReservations=len(folio.reservation_ids), + reservationType=folio.reservation_type, + closureReasonId=folio.closure_reason_id, + agencyId=folio.agency_id.id if folio.agency_id else None, + pricelistId=folio.pricelist_id.id if folio.pricelist_id else None, + saleChannelId=folio.sale_channel_origin_id.id + if folio.sale_channel_origin_id + else None, + firstCheckin=str(folio.first_checkin), + lastCheckout=str(folio.last_checkout), + createDate=folio.create_date.isoformat(), + ) + ) + return result_folios + + @restapi.method( + [ + ( + [ + "/num-last-received-folios", + ], + "GET", + ), + ], + input_param=Datamodel("pms.folio.search.param", is_list=False), + auth="jwt_api_pms", + ) + def get_num_last_received_folios(self, pms_folio_search_param): + return self.env["pms.folio"].search_count( + [ + ("first_checkin", ">=", datetime.now().date()), + ("pms_property_id", "=", pms_folio_search_param.pmsPropertyId), + ("reservation_type", "=", "normal"), + ], + ) diff --git a/pms_api_rest/services/pms_extra_bed_service.py b/pms_api_rest/services/pms_extra_bed_service.py new file mode 100644 index 0000000000..2d80863b64 --- /dev/null +++ b/pms_api_rest/services/pms_extra_bed_service.py @@ -0,0 +1,73 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsExtraBedService(Component): + _inherit = "base.rest.service" + _name = "pms.extra.beds.service" + _usage = "extra-beds" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.extra.beds.search.param"), + output_param=Datamodel("pms.extra.bed.info", is_list=True), + auth="jwt_api_pms", + ) + def get_extra_beds(self, extra_beds_search_param): + domain = [("is_extra_bed", "=", True)] + if extra_beds_search_param.name: + domain.append(("name", "like", extra_beds_search_param.name)) + if extra_beds_search_param.pmsPropertyId: + domain.extend( + [ + "|", + ("pms_property_ids", "in", extra_beds_search_param.pmsPropertyId), + ("pms_property_ids", "=", False), + ] + ) + + result_extra_beds = [] + PmsExtraBed = self.env.datamodels["pms.extra.bed.info"] + + for bed in self.env["product.product"].search( + domain, + ): + avail = -1 + if extra_beds_search_param.dateFrom and extra_beds_search_param.dateTo: + qty_for_day = self.env["pms.service.line"].read_group( + [ + ("product_id", "=", bed.id), + ("date", ">=", extra_beds_search_param.dateFrom), + ("date", "<=", extra_beds_search_param.dateTo), + ("cancel_discount", "=", 0), + ], + ["day_qty:sum"], + ["date:day"], + ) + max_daily_used = ( + max(date["day_qty"] for date in qty_for_day) if qty_for_day else 0 + ) + + avail = bed.daily_limit - max_daily_used + # Avoid send negative values in avail + avail = avail if avail >= 0 else 0 + + result_extra_beds.append( + PmsExtraBed( + id=bed.id, + name=bed.name, + dailyLimitConfig=bed.daily_limit, + dailyLimitAvail=avail, + ) + ) + + return result_extra_beds diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py new file mode 100644 index 0000000000..0bb1c4207b --- /dev/null +++ b/pms_api_rest/services/pms_folio_service.py @@ -0,0 +1,2589 @@ +import base64 +import logging +from datetime import datetime, timedelta + +import pytz + +from odoo import _, fields +from odoo.exceptions import AccessError, MissingError, ValidationError +from odoo.osv import expression +from odoo.tools import get_lang + +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.addons.portal.controllers.portal import CustomerPortal + +from ..pms_api_rest_utils import url_image_pms_api_rest + +_logger = logging.getLogger(__name__) + + +def is_adult(birthdate): + if not birthdate: + return False + today = datetime.now() + age = ( + today.year + - birthdate.year + - ((today.month, today.day) < (birthdate.month, birthdate.day)) + ) + return age >= 18 + + +class PmsFolioService(Component): + _inherit = "base.rest.service" + _name = "pms.folio.service" + _usage = "folios" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.folio.info", is_list=False), + auth="jwt_api_pms", + ) + def get_folio(self, folio_id): + folio = self.env["pms.folio"].search( + [ + ("id", "=", folio_id), + ] + ) + if folio: + portal_url = ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url") + + folio.get_portal_url() + ) + PmsFolioInfo = self.env.datamodels["pms.folio.info"] + return PmsFolioInfo( + id=folio.id, + name=folio.name, + partnerId=folio.partner_id if folio.partner_id else None, + partnerName=folio.partner_name if folio.partner_name else None, + partnerPhone=folio.mobile if folio.mobile else None, + partnerEmail=folio.email if folio.email else None, + state=folio.state, + amountTotal=round(folio.amount_total, 2), + reservationType=folio.reservation_type, + pendingAmount=folio.pending_amount, + firstCheckin=str(folio.first_checkin), + lastCheckout=str(folio.last_checkout), + createDate=folio.create_date.isoformat(), + createdBy=folio.create_uid.name, + internalComment=folio.internal_comment + if folio.internal_comment + else None, + invoiceStatus=folio.invoice_status, + pricelistId=folio.pricelist_id if folio.pricelist_id else None, + saleChannelId=folio.sale_channel_origin_id + if folio.sale_channel_origin_id + else None, + agencyId=folio.agency_id if folio.agency_id else None, + externalReference=folio.external_reference + if folio.external_reference + else None, + closureReasonId=folio.closure_reason_id, + outOfServiceDescription=folio.out_service_description + if folio.out_service_description + else None, + portalUrl=portal_url, + accessToken=folio.access_token, + language=folio.lang if folio.lang else None, + ) + else: + raise MissingError(_("Folio not found")) + + @restapi.method( + [ + ( + [ + "//adults", + ], + "GET", + ) + ], + output_param=Datamodel("pms.checkin.partner.info", is_list=True), + auth="jwt_api_pms", + ) + def get_folio_adults(self, folio_id): + folio_record = self.env["pms.folio"].search( + [ + ("id", "=", folio_id), + ] + ) + if not folio_record: + raise MissingError(_("Folio not found")) + result = [] + for checkin_partner_record in folio_record.checkin_partner_ids: + if is_adult(checkin_partner_record.birthdate_date): + result.append( + self.env.datamodels["pms.checkin.partner.info"]( + id=checkin_partner_record.id, + name=checkin_partner_record.name or "", + firstname=checkin_partner_record.firstname or "", + lastname=checkin_partner_record.lastname or "", + lastname2=checkin_partner_record.lastname2 or "", + ) + ) + return result + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.folio.search.param", is_list=False), + output_param=Datamodel("pms.folio.short.info", is_list=True), + auth="jwt_api_pms", + ) + def get_folios(self, folio_search_param): + domain_fields = list() + pms_property_id = int(folio_search_param.pmsPropertyId) + domain_fields.append(("pms_property_id", "=", pms_property_id)) + order_field = "write_date desc" + if folio_search_param.last: + order_field = "create_date desc" + if folio_search_param.dateTo and folio_search_param.dateFrom: + date_from = fields.Date.from_string(folio_search_param.dateFrom) + date_to = fields.Date.from_string(folio_search_param.dateTo) + dates = [ + date_from + timedelta(days=x) + for x in range(0, (date_to - date_from).days + 1) + ] + self.env.cr.execute( + """ + SELECT folio.id + FROM pms_reservation_line night + LEFT JOIN pms_reservation reservation + ON reservation.id = night.reservation_id + LEFT JOIN pms_folio folio + ON folio.id = reservation.folio_id + WHERE (night.pms_property_id = %s) + AND (night.date in %s) + GROUP BY folio.id + """, + ( + pms_property_id, + tuple(dates), + ), + ) + folio_ids = [x[0] for x in self.env.cr.fetchall()] + domain_fields.append(("folio_id", "in", folio_ids)) + + if folio_search_param.createDateFrom and folio_search_param.createDateTo: + domain_fields.append( + ( + "create_date", + ">=", + datetime.strptime( + folio_search_param.createDateFrom, "%Y-%m-%d %H:%M:%S" + ), + ) + ) + domain_fields.append( + ( + "create_date", + "<=", + datetime.strptime( + folio_search_param.createDateTo, "%Y-%m-%d %H:%M:%S" + ), + ) + ) + + domain_filter = list() + if folio_search_param.last: + domain_filter.append([("checkin", ">=", fields.Date.today())]) + + if folio_search_param.ids: + domain_filter.append([("folio_id", "in", folio_search_param.ids)]) + + if folio_search_param.lastUpdateFrom: + last_update_from = fields.Datetime.from_string( + folio_search_param.lastUpdateFrom + ) + domain_filter.append([("write_date", ">=", last_update_from)]) + + if folio_search_param.filter: + target = folio_search_param.filter + if "@" in target: + domain_filter.append([("email", "ilike", target)]) + else: + subdomains = [ + [("name", "ilike", target)], + [("partner_name", "ilike", "%".join(target.split(" ")))], + [("mobile", "ilike", target)], + [("external_reference", "ilike", target)], + ] + domain_filter.append(expression.OR(subdomains)) + if folio_search_param.filterByState: + if folio_search_param.filterByState == "checkinYesterday": + subdomains = [ + [("state", "in", ("confirm", "arrival_delayed"))], + [("checkin", "=", fields.Date.today() - timedelta(days=1))], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "pendingCheckinToday": + subdomains = [ + [("state", "in", ("confirm", "arrival_delayed"))], + [("checkin", "=", fields.Date.today())], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "completedCheckinsToday": + subdomains = [ + [("state", "=", "onboard")], + [("checkin", "=", fields.Date.today())], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "pendingCheckinsTomorrow": + subdomains = [ + [("state", "=", "confirm")], + [("checkin", "=", fields.Date.today() + timedelta(days=1))], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "pendingCheckoutsToday": + subdomains = [ + [("state", "in", ("onboard", "departure_delayed"))], + [("checkout", "=", fields.Date.today())], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "pendingCheckoutsTomorrow": + subdomains = [ + [("state", "in", ("onboard", "departure_delayed"))], + [("checkout", "=", fields.Date.today() + timedelta(days=1))], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "completedCheckoutsToday": + subdomains = [ + [("state", "=", "done")], + [("checkout", "=", fields.Date.today())], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "completedCheckoutsTomorrow": + subdomains = [ + [("state", "=", "done")], + [("checkout", "=", fields.Date.today() + timedelta(days=1))], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "byCheckin": + subdomains = [ + [("state", "in", ("confirm", "arrival_delayed"))], + [("checkin", "<=", fields.Date.today())], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "byCheckout": + subdomains = [ + [("state", "in", ("onboard", "departure_delayed"))], + [("checkout", "=", fields.Date.today())], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "onBoard": + subdomains = [ + [("state", "in", ("onboard", "departure_delayed"))], + [("reservation_type", "!=", "out")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "toAssign": + # this domain should be the same as notification service for unassigned reservations + subdomains = [ + [("to_assign", "=", True)], + [("state", "in", ("draft", "confirm", "arrival_delayed"))], + [("reservation_type", "!=", "out")], + [("checkin", ">=", fields.Date.today())], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "cancelled": + subdomains = [ + [("state", "=", "cancel")], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "overbooking": + subdomains = [ + [("overbooking", "=", True)], + ] + domain_filter.append(expression.AND(subdomains)) + if domain_filter: + domain = expression.AND([domain_fields, domain_filter[0]]) + if folio_search_param.filter and folio_search_param.filterByState: + domain = expression.AND( + [domain_fields, domain_filter[0], domain_filter[1]] + ) + else: + domain = domain_fields + result_folios = [] + reservations_result = ( + self.env["pms.reservation"].search(domain).mapped("folio_id").ids + ) + PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] + for folio in self.env["pms.folio"].search( + [("id", "in", reservations_result)], + order=order_field, + limit=folio_search_param.limit, + offset=folio_search_param.offset, + ): + reservations = [] + for reservation in folio.reservation_ids: + reservations.append( + { + "id": reservation.id, + "checkin": datetime.combine( + reservation.checkin, datetime.min.time() + ).isoformat(), + "checkout": datetime.combine( + reservation.checkout, datetime.min.time() + ).isoformat(), + "stateCode": reservation.state, + "cancelledReason": reservation.cancelled_reason + if reservation.cancelled_reason + else None, + "preferredRoomId": reservation.preferred_room_id.id + if reservation.preferred_room_id + else None, + "roomTypeId": reservation.room_type_id.id + if reservation.room_type_id + else None, + "roomTypeClassId": reservation.room_type_id.sudo().class_id.id + if reservation.room_type_id + else None, + "folioSequence": reservation.folio_sequence, + "adults": reservation.adults, + "priceTotal": reservation.price_total, + "pricelistId": reservation.pricelist_id.id + if reservation.pricelist_id + else None, + "saleChannelId": reservation.sale_channel_origin_id.id + if reservation.sale_channel_origin_id + else None, + "agencyId": reservation.agency_id.id + if reservation.agency_id + else None, + "isSplitted": reservation.splitted, + "toAssign": reservation.to_assign, + "reservationType": reservation.reservation_type, + "nights": reservation.nights, + "numServices": len(reservation.service_ids) + if reservation.service_ids + else 0, + "overbooking": reservation.overbooking, + "partnerId": reservation.partner_id.id + if reservation.partner_id + else None, + "isReselling": any( + line.is_reselling + for line in reservation.reservation_line_ids + ), + "isBlocked": reservation.blocked, + } + ) + result_folios.append( + PmsFolioShortInfo( + id=folio.id, + name=folio.name, + state=folio.state, + partnerName=folio.partner_name if folio.partner_name else None, + partnerPhone=folio.mobile if folio.mobile else None, + partnerEmail=folio.email if folio.email else None, + amountTotal=round(folio.amount_total, 2), + pendingAmount=round(folio.pending_amount, 2), + reservations=[] if not reservations else reservations, + paymentStateCode=folio.payment_state, + paymentStateDescription=dict( + folio.fields_get(["payment_state"])["payment_state"][ + "selection" + ] + )[folio.payment_state], + reservationType=folio.reservation_type, + closureReasonId=folio.closure_reason_id, + agencyId=folio.agency_id.id if folio.agency_id else None, + pricelistId=folio.pricelist_id.id if folio.pricelist_id else None, + saleChannelId=folio.sale_channel_origin_id.id + if folio.sale_channel_origin_id + else None, + firstCheckin=str(folio.first_checkin), + lastCheckout=str(folio.last_checkout), + createHour=folio.create_date.strftime("%H:%M"), + createDate=folio.create_date.isoformat(), + ) + ) + return result_folios + + @restapi.method( + [ + ( + [ + "//transactions", + ], + "GET", + ) + ], + input_param=Datamodel("pms.search.param"), + output_param=Datamodel("pms.transaction.info", is_list=True), + auth="jwt_api_pms", + ) + def get_folio_transactions(self, folio_id, pms_search_param): + domain = list() + domain.append(("id", "=", folio_id)) + if pms_search_param.pmsPropertyId: + domain.append(("pms_property_id", "=", pms_search_param.pmsPropertyId)) + folio = self.env["pms.folio"].search(domain) + transactions = [] + PmsTransactiontInfo = self.env.datamodels["pms.transaction.info"] + if not folio: + pass + else: + # if folio.payment_state == "not_paid": + # pass + # else: + if folio.payment_ids: + for payment in folio.payment_ids.filtered( + lambda p: p.state == "posted" + ): + payment._compute_pms_api_transaction_type() + transactions.append( + PmsTransactiontInfo( + id=payment.id, + amount=round(payment.amount, 2), + journalId=payment.journal_id.id, + date=datetime.combine( + payment.date, datetime.min.time() + ).isoformat(), + transactionType=payment.pms_api_transaction_type, + partnerId=payment.partner_id.id + if payment.partner_id + else None, + partnerName=payment.partner_id.name + if payment.partner_id + else None, + reference=payment.ref if payment.ref else None, + isReconcilied=(payment.reconciled_statements_count > 0), + downPaymentInvoiceId=payment.reconciled_invoice_ids.filtered( + lambda inv: inv._is_downpayment() + ), + ) + ) + return transactions + + @restapi.method( + [ + ( + [ + "//charge", + ], + "POST", + ) + ], + input_param=Datamodel("pms.transaction.info", is_list=False), + auth="jwt_api_pms", + ) + def create_folio_charge(self, folio_id, pms_account_payment_info): + folio = self.env["pms.folio"].browse(folio_id) + partner_id = self.env["res.partner"].browse(pms_account_payment_info.partnerId) + journal = self.env["account.journal"].browse(pms_account_payment_info.journalId) + reservations = ( + self.env["pms.reservation"].browse(pms_account_payment_info.reservationIds) + if pms_account_payment_info.reservationIds + else False + ) + if journal.type == "cash": + # REVIEW: Temporaly, if not cash session open, create a new one automatically + # Review this in pms_folio_service (/charge & /refund) + # and in pms_transaction_service (POST) + last_session = self._get_last_cash_session(journal_id=journal.id) + if last_session.state != "open": + self._action_open_cash_session( + pms_property_id=folio.pms_property_id.id, + amount=last_session.balance_end_real, + journal_id=journal.id, + force=False, + ) + self.env["pms.folio"].sudo().do_payment( + journal, + journal.suspense_account_id, + self.env.user, + pms_account_payment_info.amount, + folio, + reservations=reservations, + services=False, + partner=partner_id, + date=datetime.strptime(pms_account_payment_info.date, "%Y-%m-%d"), + ) + folio_transactions = folio.payment_ids.filtered( + lambda p: p.pms_api_transaction_type == "customer_inbound" + ) + return folio_transactions.ids + + @restapi.method( + [ + ( + [ + "//refund", + ], + "POST", + ) + ], + input_param=Datamodel("pms.transaction.info", is_list=False), + auth="jwt_api_pms", + ) + def create_folio_refund(self, folio_id, pms_account_payment_info): + folio = self.env["pms.folio"].browse(folio_id) + partner_id = self.env["res.partner"].browse(pms_account_payment_info.partnerId) + journal = self.env["account.journal"].browse(pms_account_payment_info.journalId) + if journal.type == "cash": + # REVIEW: Temporaly, if not cash session open, create a new one automatically + # Review this in pms_folio_service (/charge & /refund) + # and in pms_transaction_service (POST) + last_session = self._get_last_cash_session(journal_id=journal.id) + if last_session.state != "open": + self._action_open_cash_session( + pms_property_id=folio.pms_property_id.id, + amount=last_session.balance_end_real, + journal_id=journal.id, + force=False, + ) + self.env["pms.folio"].sudo().do_refund( + journal, + journal.suspense_account_id, + self.env.user, + pms_account_payment_info.amount, + folio, + reservations=False, + services=False, + partner=partner_id, + date=datetime.strptime(pms_account_payment_info.date, "%Y-%m-%d"), + ref=pms_account_payment_info.reference, + ) + + @restapi.method( + [ + ( + [ + "//reservations", + ], + "GET", + ) + ], + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="jwt_api_pms", + ) + def get_folio_reservations(self, folio_id): + folio = self.env["pms.folio"].browse(folio_id) + reservations = [] + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + if not folio: + pass + else: + if folio.reservation_ids: + for reservation in sorted( + folio.reservation_ids, key=lambda r: r.folio_sequence + ): + reservation_room_type_class_id = self.env[ + "pms.room.type.class" + ].browse(reservation.room_type_id.sudo().class_id.id) + reservations.append( + PmsReservationShortInfo( + id=reservation.id, + boardServiceId=reservation.board_service_room_id.id + if reservation.board_service_room_id + else None, + checkin=datetime.combine( + reservation.checkin, datetime.min.time() + ).isoformat(), + checkout=datetime.combine( + reservation.checkout, datetime.min.time() + ).isoformat(), + roomTypeId=reservation.room_type_id.id + if reservation.room_type_id + else None, + roomTypeClassId=reservation_room_type_class_id + if reservation_room_type_class_id + else None, + preferredRoomId=reservation.preferred_room_id.id + if reservation.preferred_room_id + else None, + name=reservation.name, + adults=reservation.adults, + stateCode=reservation.state, + stateDescription=dict( + reservation.fields_get(["state"])["state"]["selection"] + )[reservation.state], + children=reservation.children + if reservation.children + else 0, + readyForCheckin=reservation.ready_for_checkin, + allowedCheckout=reservation.allowed_checkout, + isSplitted=reservation.splitted, + priceTotal=round(reservation.price_room_services_set, 2), + folioSequence=reservation.folio_sequence + if reservation.folio_sequence + else None, + pricelistId=reservation.pricelist_id, + servicesCount=sum( + reservation.service_ids.filtered( + lambda x: not x.is_board_service + ).mapped("product_qty") + ), + nights=reservation.nights, + numServices=len(reservation.service_ids) + if reservation.service_ids + else 0, + toAssign=reservation.to_assign, + overbooking=reservation.overbooking, + isBlocked=reservation.blocked, + reservationType=reservation.reservation_type, + segmentationId=reservation.segmentation_ids[0].id + if reservation.segmentation_ids + else None, + isOverNightRoom=reservation.overnight_room, + partnerId=reservation.partner_id.id + if reservation.partner_id + else None, + ) + ) + + return reservations + + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.folio.info", is_list=False), + auth="jwt_api_pms", + ) + # flake8:noqa=C901 + 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.externalReference and external_app: + # If folio exists (external_reference + pms_property_id) + # ignore the creation of the folio and log the payload with "duplicate" mensaje + folio = self.env["pms.folio"].search( + [ + ("external_reference", "=", pms_folio_info.externalReference), + ("pms_property_id", "=", pms_folio_info.pmsPropertyId), + ("agency_id", "=", pms_folio_info.agencyId), + ] + ) + if folio: + _logger.info( + "Folio with external reference %s and property %s already exists", + pms_folio_info.externalReference, + pms_folio_info.pmsPropertyId, + ) + raise ValidationError(_("Folio already exists")) + agency = False + if pms_folio_info.agencyId: + agency = self.env["res.partner"].browse(pms_folio_info.agencyId) + if pms_folio_info.reservationType == "out": + vals = { + "pms_property_id": pms_folio_info.pmsPropertyId, + "reservation_type": pms_folio_info.reservationType, + "closure_reason_id": pms_folio_info.closureReasonId, + "out_service_description": pms_folio_info.outOfServiceDescription + if pms_folio_info.outOfServiceDescription + else None, + } + else: + vals = { + "pms_property_id": pms_folio_info.pmsPropertyId, + "agency_id": agency.id if agency else False, + "sale_channel_origin_id": self.get_channel_origin_id( + pms_folio_info.saleChannelId, pms_folio_info.agencyId + ), + "reservation_type": pms_folio_info.reservationType or "normal", + "external_reference": pms_folio_info.externalReference, + "internal_comment": pms_folio_info.internalComment, + "lang": self.get_language(pms_folio_info.language), + } + + if pms_folio_info.partnerId: + vals.update( + { + "partner_id": pms_folio_info.partnerId, + } + ) + else: + if pms_folio_info.partnerName: + vals.update( + { + "partner_name": pms_folio_info.partnerName, + } + ) + if pms_folio_info.partnerPhone: + vals.update( + { + "mobile": pms_folio_info.partnerPhone, + } + ) + if pms_folio_info.partnerEmail: + vals.update( + { + "email": pms_folio_info.partnerEmail, + } + ) + folio = self.env["pms.folio"].create(vals) + for reservation in pms_folio_info.reservations: + commision_percent_to_deduct = 0 + if external_app and agency and agency.commission_type == "subtract": + commision_percent_to_deduct = agency.default_commission + vals = { + "folio_id": folio.id, + "room_type_id": reservation.roomTypeId, + "pms_property_id": pms_folio_info.pmsPropertyId, + "pricelist_id": pms_folio_info.pricelistId, + "external_reference": pms_folio_info.externalReference, + "board_service_room_id": self.get_board_service_room_type_id( + reservation.boardServiceId, + reservation.roomTypeId, + pms_folio_info.pmsPropertyId, + ), + "preferred_room_id": reservation.preferredRoomId, + "adults": reservation.adults, + "reservation_type": pms_folio_info.reservationType or "normal", + "children": reservation.children, + "preconfirm": pms_folio_info.preconfirm, + "blocked": True if external_app else False, + "partner_requests": reservation.partnerRequests or "", + } + if reservation.reservationLines: + vals_lines = [] + board_day_price = 0 + # The service price is included in day price when it is a board service (external api) + if external_app and vals.get("board_service_room_id"): + board = self.env["pms.board.service.room.type"].browse( + vals["board_service_room_id"] + ) + if reservation.adults: + board_day_price += ( + sum( + board.board_service_line_ids.with_context( + property=folio.pms_property_id.id + ) + .filtered(lambda l: l.adults) + .mapped("amount") + ) + * reservation.adults + ) + if reservation.children: + board_day_price += ( + sum( + board.board_service_line_ids.with_context( + property=folio.pms_property_id.id + ) + .filtered(lambda l: l.children) + .mapped("amount") + ) + * reservation.children + ) + for reservationLine in reservation.reservationLines: + price = reservationLine.price - ( + commision_percent_to_deduct * reservationLine.price / 100 + ) + vals_lines.append( + ( + 0, + 0, + { + "date": reservationLine.date, + "price": price - board_day_price, + "discount": reservationLine.discount, + }, + ) + ) + vals["reservation_line_ids"] = vals_lines + else: + vals["checkin"] = reservation.checkin + vals["checkout"] = reservation.checkout + + reservation_record = ( + self.env["pms.reservation"] + .with_context( + skip_compute_board_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, + ) + .create(vals) + ) + if reservation.services: + for service in reservation.services: + if service.serviceLines: + vals = { + "product_id": service.productId, + "reservation_id": reservation_record.id, + "is_board_service": service.isBoardService, + "board_service_line_id": service.boardServiceLineId, + "service_line_ids": [ + ( + 0, + False, + { + "date": line.date, + "price_unit": line.priceUnit, + "discount": line.discount or 0, + "day_qty": line.quantity, + }, + ) + for line in service.serviceLines + ], + } + self.env["pms.service"].create(vals) + else: + product = self.env["product.product"].browse( + service.productId + ) + vals = { + "product_id": service.productId, + "reservation_id": reservation_record.id, + "discount": service.discount or 0, + } + if not (product.per_day or product.per_person): + vals.update( + { + "product_qty": service.quantity, + } + ) + new_service = self.env["pms.service"].create(vals) + new_service.service_line_ids.price_unit = service.priceUnit + # Force compute board service default if not board service is set + # REVIEW: Precharge the board service in the app form? + if ( + not reservation_record.board_service_room_id + or reservation_record.board_service_room_id == 0 + ): + reservation_record.with_context( + skip_compute_board_service_ids=False, + force_write_blocked=True if external_app else False, + )._compute_board_service_room_id() + if reservation.stateCode == "cancel": + reservation_record.action_cancel() + 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) + if pms_folio_info.state == "cancel": + folio.action_cancel() + # REVIEW: analyze how to integrate the sending of mails from the API + # with the configuration of the automatic mails pms + # & + # the sending of mail should be a specific call once the folio has been created? + if folio and folio.email and pms_folio_info.sendConfirmationMail: + template = folio.pms_property_id.property_confirmed_template + if not template: + raise ValidationError( + _("There is no confirmation template for this property") + ) + email_values = { + "email_to": folio.email, + "email_from": folio.pms_property_id.email + if folio.pms_property_id.email + else False, + "auto_delete": False, + } + template.send_mail(folio.id, force_send=True, email_values=email_values) + # Mapped room types and dates to call force_api_update_avail + mapped_room_types = folio.reservation_ids.mapped("room_type_id") + date_from = min(folio.reservation_ids.mapped("checkin")) + date_to = max(folio.reservation_ids.mapped("checkout")) + self.force_api_update_avail( + pms_property_id=pms_folio_info.pmsPropertyId, + room_type_ids=mapped_room_types.ids, + date_from=date_from, + date_to=date_to, + ) + if external_app: + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": pms_folio_info.pmsPropertyId, + "client_id": self.env.user.id, + "request": log_payload, + "response": folio.id, + "status": "success", + "request_date": fields.Datetime.now(), + "method": "POST", + "endpoint": "/folios", + "folio_ids": folio.ids, + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", + } + ) + return folio.id + except Exception as e: + _logger.error( + "Error creating folio from API: %s", + e, + exc_info=True, + ) + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": pms_folio_info.pmsPropertyId, + "client_id": self.env.user.id, + "request": log_payload, + "response": e, + "status": "error", + "request_date": fields.Datetime.now(), + "method": "POST", + "endpoint": "/folios", + "folio_ids": [], + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", + } + ) + if not external_app: + raise ValidationError(_("Error creating folio from API: %s") % e) + else: + return 0 + + def compute_transactions(self, folio, transactions): + for transaction in transactions: + reference = folio.name + " - " + if transaction.reference: + reference += transaction.reference + else: + raise ValidationError(_("The transaction reference is required")) + 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 + 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", reference), + ("state", "=", "posted"), + ("create_uid", "=", self.env.user.id), + ("journal_id", "=", journal.id), + ] + ) + if ( + not proposed_transaction + or proposed_transaction.amount != transaction.amount + ): + if proposed_transaction: + proposed_transaction.action_draft() + proposed_transaction.amount = transaction.amount + proposed_transaction.action_post() + else: + if transaction.transactionType == "inbound": + folio.sudo().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( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.folio.info", is_list=False), + auth="jwt_api_pms", + ) + # flake8:noqa=C901 + def update_folio(self, folio_id, pms_folio_info): + folio = self.env["pms.folio"].browse(folio_id) + folio_vals = {} + if not folio: + raise MissingError(_("Folio not found")) + if pms_folio_info.cancelReservations: + folio.action_cancel() + if pms_folio_info.confirmReservations: + for reservation in folio.reservation_ids: + 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: + folio_vals.update({"partner_id": pms_folio_info.partnerId}) + else: + if folio.partner_id: + folio.partner_id = False + if pms_folio_info.partnerName is not None: + folio_vals.update({"partner_name": pms_folio_info.partnerName}) + if pms_folio_info.partnerEmail is not None: + folio_vals.update({"email": pms_folio_info.partnerEmail}) + if pms_folio_info.partnerPhone is not None: + folio_vals.update({"mobile": pms_folio_info.partnerPhone}) + if pms_folio_info.language: + folio_vals.update({"lang": pms_folio_info.language}) + if pms_folio_info.reservations: + for reservation in pms_folio_info.reservations: + vals = { + "folio_id": folio.id, + "room_type_id": reservation.roomTypeId, + "checkin": reservation.checkin, + "checkout": reservation.checkout, + "pms_property_id": pms_folio_info.pmsPropertyId, + "pricelist_id": pms_folio_info.pricelistId, + "external_reference": pms_folio_info.externalReference, + "board_service_room_id": reservation.boardServiceId, + "preferred_room_id": reservation.preferredRoomId, + "adults": reservation.adults, + "reservation_type": pms_folio_info.reservationType, + "children": reservation.children, + } + reservation_record = self.env["pms.reservation"].create(vals) + if reservation.services: + for service in reservation.services: + vals = { + "product_id": service.productId, + "reservation_id": reservation_record.id, + "is_board_service": False, + "service_line_ids": [ + ( + 0, + False, + { + "date": line.date, + "price_unit": line.priceUnit, + "discount": line.discount or 0, + "day_qty": line.quantity, + }, + ) + for line in service.serviceLines + ], + } + self.env["pms.service"].create(vals) + if folio_vals: + folio.write(folio_vals) + + # ------------------------------------------------------------------------------------ + # FOLIO SERVICES---------------------------------------------------------------- + # ------------------------------------------------------------------------------------ + + @restapi.method( + [ + ( + [ + "//services", + ], + "GET", + ) + ], + output_param=Datamodel("pms.service.info", is_list=True), + auth="jwt_api_pms", + ) + def get_folio_services(self, folio_id): + folio = self.env["pms.folio"].search([("id", "=", folio_id)]) + if not folio: + raise MissingError(_("Folio not found")) + + result_services = [] + PmsServiceInfo = self.env.datamodels["pms.service.info"] + for reservation in folio.reservation_ids: + for service in reservation.service_ids: + PmsServiceLineInfo = self.env.datamodels["pms.service.line.info"] + service_lines = [] + for line in service.service_line_ids: + service_lines.append( + PmsServiceLineInfo( + id=line.id, + date=datetime.combine( + line.date, datetime.min.time() + ).isoformat(), + priceUnit=line.price_unit, + discount=line.discount, + quantity=line.day_qty, + ) + ) + + result_services.append( + PmsServiceInfo( + id=service.id, + reservationId=service.reservation_id, + name=service.name, + productId=service.product_id.id, + quantity=service.product_qty, + priceTotal=round(service.price_total, 2), + priceSubtotal=round(service.price_subtotal, 2), + priceTaxes=round(service.price_tax, 2), + discount=round(service.discount, 2), + isBoardService=service.is_board_service, + serviceLines=service_lines, + ) + ) + return result_services + + @restapi.method( + [ + ( + [ + "//mail", + ], + "GET", + ) + ], + input_param=Datamodel("pms.mail.info"), + output_param=Datamodel("pms.mail.info", is_list=False), + auth="jwt_api_pms", + ) + def get_folio_mail(self, folio_id, pms_mail_info): + folio = self.env["pms.folio"].browse(folio_id) + if pms_mail_info.mailType == "confirm": + compose_vals = { + "template_id": folio.pms_property_id.property_confirmed_template.id, + "model": "pms.folio", + "res_ids": folio.id, + } + elif pms_mail_info.mailType == "done": + compose_vals = { + "template_id": folio.pms_property_id.property_exit_template.id, + "model": "pms.folio", + "res_ids": folio.id, + } + elif pms_mail_info.mailType == "cancel": + # TODO: only send first cancel reservation, not all + # the template is not ready for multiple reservations + compose_vals = { + "template_id": folio.pms_property_id.property_canceled_template.id, + "model": "pms.reservation", + "res_ids": folio.reservation_ids.filtered( + lambda r: r.state == "cancel" + )[0].id, + } + values = self.env["mail.compose.message"].generate_email_for_composer( + template_id=compose_vals["template_id"], + res_ids=compose_vals["res_ids"], + fields=["subject", "body_html"], + ) + PmsMailInfo = self.env.datamodels["pms.mail.info"] + return PmsMailInfo( + bodyMail=values["body"], + subject=values["subject"], + ) + + @restapi.method( + [ + ( + [ + "//send-mail", + ], + "POST", + ) + ], + input_param=Datamodel("pms.mail.info"), + auth="jwt_api_pms", + ) + def send_folio_mail(self, folio_id, pms_mail_info): + folio = self.env["pms.folio"].browse(folio_id) + recipients = pms_mail_info.emailAddresses + + email_values = { + "email_to": ",".join(recipients) if recipients else False, + "partner_ids": pms_mail_info.partnerIds + if pms_mail_info.partnerIds + else False, + "recipient_ids": pms_mail_info.partnerIds + if pms_mail_info.partnerIds + else False, + "auto_delete": False, + } + if pms_mail_info.bodyMail: + email_values.update( + { + "body": pms_mail_info.bodyMail, + "body_html": pms_mail_info.bodyMail, + } + ) + if pms_mail_info.mailType == "confirm": + template = folio.pms_property_id.property_confirmed_template + res_id = folio.id + template.send_mail(res_id, force_send=True, email_values=email_values) + elif pms_mail_info.mailType == "done": + template = folio.pms_property_id.property_exit_template + res_id = folio.id + template.send_mail(res_id, force_send=True, email_values=email_values) + if pms_mail_info.mailType == "cancel": + template = folio.pms_property_id.property_canceled_template + res = folio.reservation_ids.filtered(lambda r: r.state == "cancel") + res_id = res[0].id + template.send_mail(res_id, force_send=True, email_values=email_values) + return True + + @restapi.method( + [ + ( + [ + "//sale-lines", + ], + "GET", + ) + ], + output_param=Datamodel("pms.folio.sale.line.info", is_list=True), + auth="jwt_api_pms", + ) + def get_folio_sale_lines(self, folio_id): + folio = self.env["pms.folio"].browse(folio_id) + sale_lines = [] + if not folio: + pass + else: + PmsFolioSaleLineInfo = self.env.datamodels["pms.folio.sale.line.info"] + if folio.sale_line_ids: + for sale_line in folio.sale_line_ids: + sale_lines.append( + PmsFolioSaleLineInfo( + id=sale_line.id if sale_line.id else None, + name=sale_line.name if sale_line.name else None, + priceUnit=sale_line.price_unit + if sale_line.price_unit + else None, + qtyToInvoice=self._get_section_qty_to_invoice(sale_line) + if sale_line.display_type == "line_section" + else sale_line.qty_to_invoice, + qtyInvoiced=sale_line.qty_invoiced + if sale_line.qty_invoiced + else None, + priceTotal=sale_line.price_total + if sale_line.price_total + else None, + discount=sale_line.discount if sale_line.discount else None, + productQty=sale_line.product_uom_qty + if sale_line.product_uom_qty + else None, + reservationId=sale_line.reservation_id + if sale_line.reservation_id + else None, + serviceId=sale_line.service_id + if sale_line.service_id + else None, + displayType=sale_line.display_type + if sale_line.display_type + else None, + defaultInvoiceTo=sale_line.default_invoice_to + if sale_line.default_invoice_to + else None, + isDownPayment=sale_line.is_downpayment, + ) + ) + + return sale_lines + + @restapi.method( + [ + ( + [ + "//invoices", + ], + "GET", + ) + ], + output_param=Datamodel("pms.invoice.info", is_list=True), + auth="jwt_api_pms", + ) + def get_folio_invoices(self, folio_id): + folio = self.env["pms.folio"].browse(folio_id) + invoices = [] + if not folio: + pass + else: + PmsFolioInvoiceInfo = self.env.datamodels["pms.invoice.info"] + PmsInvoiceLineInfo = self.env.datamodels["pms.invoice.line.info"] + if folio.move_ids: + for move in folio.move_ids: + move_lines = [] + for move_line in move.invoice_line_ids: + move_lines.append( + PmsInvoiceLineInfo( + id=move_line.id, + name=move_line.name if move_line.name else None, + quantity=move_line.quantity + if move_line.quantity + else None, + priceUnit=move_line.price_unit + if move_line.price_unit + else None, + total=move_line.price_total + if move_line.price_total + else None, + discount=move_line.discount + if move_line.discount + else None, + displayType=move_line.display_type + if move_line.display_type + else None, + saleLineId=move_line.folio_line_ids[0] + if move_line.folio_line_ids + else None, + isDownPayment=move_line.move_id._is_downpayment(), + ) + ) + move_url = ( + move.get_proforma_portal_url() + if move.state == "draft" + else move.get_portal_url() + ) + portal_url = ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url") + + move_url + ) + invoice_date = ( + move.invoice_date.strftime("%d/%m/%Y") + if move.invoice_date + else move.invoice_date_due.strftime("%d/%m/%Y") + if move.invoice_date_due + else None + ) + invoices.append( + PmsFolioInvoiceInfo( + id=move.id if move.id else None, + name=move.name if move.name else None, + amount=round(move.amount_total, 2) + if move.amount_total + else None, + date=invoice_date, + state=move.state if move.state else None, + paymentState=move.payment_state + if move.payment_state + else None, + partnerName=move.partner_id.name + if move.partner_id.name + else None, + partnerId=move.partner_id.id + if move.partner_id.id + else None, + moveLines=move_lines if move_lines else None, + portalUrl=portal_url, + moveType=move.move_type, + isReversed=move.payment_state == "reversed", + isDownPaymentInvoice=move._is_downpayment(), + isSimplifiedInvoice=move.journal_id.is_simplified_invoice, + narration=move.narration if move.narration else None, + ) + ) + return invoices + + @restapi.method( + [ + ( + [ + "//invoices", + ], + "POST", + ) + ], + input_param=Datamodel("pms.invoice.info", is_list=False), + auth="jwt_api_pms", + ) + def create_folio_invoices(self, folio_id, invoice_info): + # TODO: Missing payload data: + # - date format is in invoice_info but dont save + # - invoice comment is in invoice_info but dont save + lines_to_invoice_dict = dict() + for item in invoice_info.saleLines: + if item.qtyToInvoice: + lines_to_invoice_dict[item.id] = item.qtyToInvoice + + sale_lines_to_invoice = self.env["folio.sale.line"].browse( + lines_to_invoice_dict.keys() + ) + for line in sale_lines_to_invoice: + if line.section_id and line.section_id.id not in sale_lines_to_invoice.ids: + sale_lines_to_invoice |= line.section_id + lines_to_invoice_dict[line.section_id.id] = 0 + folios_to_invoice = sale_lines_to_invoice.folio_id + invoices = folios_to_invoice._create_invoices( + lines_to_invoice=lines_to_invoice_dict, + partner_invoice_id=invoice_info.partnerId, + final=True, # To force take into account down payments + ) + # TODO: Proposed improvement with strong refactoring: + # modify the folio _create_invoices() method so that it allows specifying any + # lines field before creation (right now it only allows quantity), + # avoiding having to review the lines to modify them afterwards + for item in invoice_info.saleLines: + if item.id in invoices.invoice_line_ids.mapped("folio_line_ids.id"): + invoice_line = invoices.invoice_line_ids.filtered( + lambda r: item.id in r.folio_line_ids.ids + and not any([r.folio_line_ids.is_downpayment]) + # To avoid modifying down payments description + ) + if invoice_line: + invoice_line.write({"name": item.name}) + if invoice_info.narration: + invoices.write({"narration": invoice_info.narration}) + return invoices.ids + + # TODO: Used for the temporary function of auto-open cash session + # (View: charge/refund endpoints) + def _get_last_cash_session(self, journal_id, pms_property_id=False): + domain = [("journal_id", "=", journal_id)] + if pms_property_id: + domain.append(("pms_property_id", "=", pms_property_id)) + return ( + self.env["account.bank.statement"] + .sudo() + .search( + domain, + order="date desc, id desc", + limit=1, + ) + ) + + # TODO: Used for the temporary function of auto-open cash session + # (View: charge/refund endpoints)) + def _action_open_cash_session(self, pms_property_id, amount, journal_id, force): + statement = self._get_last_cash_session( + journal_id=journal_id, + pms_property_id=pms_property_id, + ) + if round(statement.balance_end_real, 2) == round(amount, 2) or force: + self.env["account.bank.statement"].sudo().create( + { + "name": datetime.today().strftime(get_lang(self.env).date_format) + + " (" + + self.env.user.login + + ")", + "date": datetime.today(), + "balance_start": amount, + "journal_id": journal_id, + "pms_property_id": pms_property_id, + } + ) + diff = round(amount - statement.balance_end_real, 2) + return {"result": True, "diff": diff} + else: + diff = round(amount - statement.balance_end_real, 2) + return {"result": False, "diff": diff} + + def _get_section_qty_to_invoice(self, sale_line): + folio = sale_line.folio_id + if sale_line.display_type == "line_section": + # Get if the section has a lines to invoice + seq = sale_line.sequence + next_line_section = folio.sale_line_ids.filtered( + lambda l: l.sequence > seq and l.display_type == "line_section" + ) + if next_line_section: + return sum( + folio.sale_line_ids.filtered( + lambda l: l.sequence > seq + and l.sequence < next_line_section[0].sequence + and l.display_type != "line_section" + ).mapped("qty_to_invoice") + ) + else: + return sum( + folio.sale_line_ids.filtered( + lambda l: l.sequence > seq and l.display_type != "line_section" + ).mapped("qty_to_invoice") + ) + return 0 + + @restapi.method( + [ + ( + [ + "//checkin-partners", + ], + "GET", + ) + ], + output_param=Datamodel("pms.checkin.partner.info", is_list=True), + auth="jwt_api_pms", + ) + def get_folio_checkin_partners(self, folio_id): + folio = self.env["pms.folio"].browse(folio_id) + checkin_partners = [] + if folio: + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] + for checkin_partner in folio.checkin_partner_ids: + checkin_partners.append( + PmsCheckinPartnerInfo( + id=checkin_partner.id, + reservationId=checkin_partner.reservation_id.id, + name=checkin_partner.name if checkin_partner.name else "", + firstname=checkin_partner.firstname + if checkin_partner.firstname + else None, + lastname=checkin_partner.lastname + if checkin_partner.lastname + else None, + lastname2=checkin_partner.lastname2 + if checkin_partner.lastname2 + else None, + email=checkin_partner.email if checkin_partner.email else "", + mobile=checkin_partner.mobile if checkin_partner.mobile else "", + documentType=checkin_partner.document_type.id + if checkin_partner.document_type + else None, + documentNumber=checkin_partner.document_number + if checkin_partner.document_number + else None, + documentExpeditionDate=datetime.combine( + checkin_partner.document_expedition_date, + datetime.min.time(), + ).isoformat() + if checkin_partner.document_expedition_date + else None, + documentSupportNumber=checkin_partner.support_number + if checkin_partner.support_number + else None, + documentCountryId=checkin_partner.document_country_id.id + if checkin_partner.document_country_id + else None, + gender=checkin_partner.gender if checkin_partner.gender else "", + birthdate=datetime.combine( + checkin_partner.birthdate_date, datetime.min.time() + ).isoformat() + if checkin_partner.birthdate_date + else None, + residenceStreet=checkin_partner.residence_street + if checkin_partner.residence_street + else None, + zip=checkin_partner.residence_zip + if checkin_partner.residence_zip + else None, + residenceCity=checkin_partner.residence_city + if checkin_partner.residence_city + else None, + nationality=checkin_partner.nationality_id.id + if checkin_partner.nationality_id + else None, + countryState=checkin_partner.residence_state_id.id + if checkin_partner.residence_state_id + else None, + countryStateName=checkin_partner.residence_state_id.name + if checkin_partner.residence_state_id + else None, + countryId=checkin_partner.residence_country_id.id + if checkin_partner.residence_country_id + else None, + checkinPartnerState=checkin_partner.state, + signature=checkin_partner.signature + if checkin_partner.signature + else None, + ) + ) + return checkin_partners + + @restapi.method( + [ + ( + [ + "//messages", + ], + "GET", + ) + ], + auth="jwt_api_pms", + output_param=Datamodel("pms.message.info", is_list=False), + ) + def get_folio_reservation_messages(self, folio_id): + reservation_messages = [] + folio_messages = [] + if folio_id: + folio = self.env["pms.folio"].browse(folio_id) + reservations = self.env["pms.reservation"].browse(folio.reservation_ids.ids) + user_tz = pytz.timezone(self.env.user.tz) + for messages in reservations.message_ids: + PmsReservationMessageInfo = self.env.datamodels[ + "pms.reservation.message.info" + ] + for message in messages: + reservation_message_date = pytz.UTC.localize(message.date) + reservation_message_date = reservation_message_date.astimezone( + user_tz + ) + message_body = self.parse_message_body(message) + if message.message_type == "email": + subject = "Email enviado: " + message.subject + else: + subject = message.subject if message.subject else None + reservation_messages.append( + PmsReservationMessageInfo( + reservationId=message.res_id, + author=message.author_id.name + if message.author_id + else message.email_from, + message=message_body, + subject=subject, + date=reservation_message_date.strftime("%d/%m/%y %H:%M:%S"), + messageType=message.message_type, + authorImageBase64=base64.b64encode( + message.author_id.image_1024 + ).decode("utf-8") + if message.author_id.image_1024 + else None, + authorImageUrl=url_image_pms_api_rest( + "res.partner", message.author_id.id, "image_1024" + ), + ) + ) + PmsFolioMessageInfo = self.env.datamodels["pms.folio.message.info"] + for folio_message in folio.message_ids: + message_body = self.parse_message_body(folio_message) + if folio_message.message_type == "email": + subject = "Email enviado: " + folio_message.subject + else: + subject = folio_message.subject if folio_message.subject else None + folio_message_date = pytz.UTC.localize(folio_message.date) + folio_message_date = folio_message_date.astimezone(user_tz) + folio_messages.append( + PmsFolioMessageInfo( + author=folio_message.author_id.name + if folio_message.author_id + else folio_message.email_from, + message=message_body, + subject=subject, + date=folio_message_date.strftime("%d/%m/%y %H:%M:%S"), + messageType=folio_message.message_type, + authorImageBase64=base64.b64encode( + folio_message.author_id.image_1024 + ).decode("utf-8") + if folio_message.author_id.image_1024 + else None, + authorImageUrl=url_image_pms_api_rest( + "res.partner", folio_message.author_id.id, "image_1024" + ), + ) + ) + PmsMessageInfo = self.env.datamodels["pms.message.info"] + return PmsMessageInfo( + folioMessages=folio_messages, + reservationMessages=reservation_messages, + ) + + def parse_message_body(self, message): + message = message.sudo() + message_body = "" + if message.body: + message_body = message.body + elif message.tracking_value_ids: + old_value = False + new_value = False + for tracking_value in message.tracking_value_ids: + if tracking_value.field_type == "float": + old_value = tracking_value.old_value_float + new_value = tracking_value.new_value_float + elif ( + tracking_value.field_type == "char" + or tracking_value.field_type == "selection" + or tracking_value.field_type == "many2one" + ): + old_value = tracking_value.old_value_char + new_value = tracking_value.new_value_char + elif tracking_value.field_type == "datetime": + old_value = tracking_value.old_value_datetime + new_value = tracking_value.new_value_datetime + elif tracking_value.field_type == "integer": + old_value = tracking_value.old_value_integer + new_value = tracking_value.new_value_integer + elif tracking_value.field_type == "monetary": + old_value = tracking_value.old_value_monetary + new_value = tracking_value.new_value_monetary + elif tracking_value.field_type == "text": + old_value = tracking_value.old_value_text + new_value = tracking_value.new_value_text + message_body += ( + "-" + + tracking_value.field.field_description + + ": " + + str(old_value) + + " => " + + str(new_value) + ) + return message_body + + def get_channel_origin_id(self, sale_channel_id, agency_id): + """ + Returns the channel origin id for the given agency + or website channel if not agency is given + (TODO change by configuration user api in the future) + """ + external_app = self.env.user.pms_api_client + if sale_channel_id: + return sale_channel_id + if not agency_id and external_app: + channel_origin_id = ( + self.env.user.partner_id.sale_channel_id.id + if self.env.user.partner_id.sale_channel_id + else self.env["pms.sale.channel"] + .search( + [("channel_type", "=", "direct"), ("is_on_line", "=", True)], + limit=1, + ) + .id + ) + return channel_origin_id + agency = self.env["res.partner"].browse(agency_id) + if agency: + return agency.sale_channel_id.id + return False + + def get_language(self, lang_code): + """ + Returns the language for the given language code + """ + external_app = self.env.user.pms_api_client + if not external_app: + return lang_code + return self.env["res.lang"].search([("iso_code", "=", lang_code)], limit=1).code + + def get_board_service_room_type_id( + self, board_service_id, room_type_id, pms_property_id + ): + """ + The internal app uses the board service room type id to create the reservation, + but the external app uses the board service id and the room type id. + Returns the board service room type id for the given board service and room type + """ + board_service = self.env["pms.board.service"].browse(board_service_id) + room_type = self.env["pms.room.type"].browse(room_type_id) + external_app = self.env.user.pms_api_client + if not external_app: + return board_service_id + if board_service and room_type: + return ( + self.env["pms.board.service.room.type"] + .search( + [ + ("pms_board_service_id", "=", board_service.id), + ("pms_room_type_id", "=", room_type.id), + ("pms_property_id", "=", pms_property_id), + ], + limit=1, + ) + .id + ) + return False + + # TEMP + + @restapi.method( + [ + ( + [ + "/external/", + ], + "PUT", + ) + ], + input_param=Datamodel("pms.folio.info", is_list=False), + auth="jwt_api_pms", + ) + 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( + [ + ("external_reference", "ilike", external_reference), + ("pms_property_id", "=", pms_folio_info.pmsPropertyId), + ("agency_id", "=", pms_folio_info.agencyId), + ] + ) + if not folio or len(folio) > 1: + raise MissingError(_("Folio not found")) + self.update_folio_values(folio, pms_folio_info) + # Force update availability + mapped_room_types = folio.reservation_ids.mapped("room_type_id") + date_from = min(folio.reservation_ids.mapped("checkin")) + date_to = max(folio.reservation_ids.mapped("checkout")) + self.force_api_update_avail( + pms_property_id=pms_folio_info.pmsPropertyId, + room_type_ids=mapped_room_types.ids, + date_from=date_from, + date_to=date_to, + ) + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": pms_folio_info.pmsPropertyId, + "client_id": self.env.user.id, + "request": log_payload, + "response": folio.id, + "status": "success", + "request_date": fields.Datetime.now(), + "method": "PUT", + "endpoint": "/folios/external/" + external_reference, + "folio_ids": folio.ids, + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", + } + ) + return folio.id + except Exception as e: + _logger.error( + "Error updating folio from API: %s", + e, + exc_info=True, + ) + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": pms_folio_info.pmsPropertyId, + "client_id": self.env.user.id, + "request": log_payload, + "response": e, + "status": "error", + "request_date": fields.Datetime.now(), + "method": "PUT", + "endpoint": "/folios/external/" + external_reference, + "folio_ids": [], + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", + } + ) + if not external_app: + raise ValidationError(_("Error updating folio from API: %s") % e) + else: + return 0 + + @restapi.method( + [ + ( + [ + "/", + ], + "PUT", + ) + ], + input_param=Datamodel("pms.folio.info", is_list=False), + auth="jwt_api_pms", + ) + 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) + # Force update availability + mapped_room_types = folio.reservation_ids.mapped("room_type_id") + date_from = min(folio.reservation_ids.mapped("checkin")) + date_to = max(folio.reservation_ids.mapped("checkout")) + self.force_api_update_avail( + pms_property_id=pms_folio_info.pmsPropertyId, + room_type_ids=mapped_room_types.ids, + date_from=date_from, + date_to=date_to, + ) + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": pms_folio_info.pmsPropertyId, + "client_id": self.env.user.id, + "request": log_payload, + "response": folio.id, + "status": "success", + "request_date": fields.Datetime.now(), + "method": "PUT", + "endpoint": "/folios" + str(folio_id), + "folio_ids": folio.ids, + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", + } + ) + + return folio.id + except Exception as e: + _logger.error( + "Error updating folio from API: %s", + e, + exc_info=True, + ) + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": log_payload.pmsPropertyId, + "client_id": self.env.user.id, + "request": log_payload, + "response": e, + "status": "error", + "request_date": fields.Datetime.now(), + "method": "PUT", + "endpoint": "/folios" + str(folio_id), + "folio_ids": [], + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", + } + ) + if not external_app: + raise ValidationError(_("Error updating folio from API: %s") % e) + else: + return 0 + + def update_folio_values(self, folio, pms_folio_info): + external_app = self.env.user.pms_api_client + folio_vals = {} + if pms_folio_info.state == "cancel" and folio.state != "cancel": + draft_invoices = folio.move_ids.filtered(lambda i: i.state == "draft") + if draft_invoices: + draft_invoices.button_cancel() + folio.action_cancel() + return folio.id + # if ( + # pms_folio_info.confirmReservations + # and any( + # reservation.state != "confirm" + # for reservation in folio.reservation_ids + # ) + # ): + # for reservation in folio.reservation_ids: + # reservation.action_confirm() + if pms_folio_info.internalComment is not None: + if not folio.internal_comment: + folio_vals.update({"internal_comment": pms_folio_info.internalComment}) + elif pms_folio_info.internalComment not in folio.internal_comment: + folio_vals.update( + { + "internal_comment": folio.internal_comment + + " " + + pms_folio_info.internalComment + } + ) + if pms_folio_info.partnerId and folio.partner_id.id != pms_folio_info.partnerId: + folio_vals.update({"partner_id": pms_folio_info.partnerId}) + elif not pms_folio_info.partnerId: + if folio.partner_id: + folio.partner_id = False + if ( + pms_folio_info.partnerName is not None + and folio.partner_name != pms_folio_info.partnerName + ): + folio_vals.update({"partner_name": pms_folio_info.partnerName}) + if ( + pms_folio_info.partnerEmail is not None + and folio.email != pms_folio_info.partnerEmail + ): + folio_vals.update({"email": pms_folio_info.partnerEmail}) + if ( + pms_folio_info.partnerPhone is not None + and folio.mobile != pms_folio_info.partnerPhone + ): + folio_vals.update({"mobile": pms_folio_info.partnerPhone}) + if ( + self.get_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: + 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, + force_overbooking=True if external_app else False, + ).browse(val[1]).action_confirm() + # delete from reservations_vals the field state + val[2].pop("state") + update_reservation_ids.append(val[1]) + old_reservations_to_cancel = folio.reservation_ids.filtered( + lambda r: r.state != "cancel" and r.id not in update_reservation_ids + ) + 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_board_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, folio + ) + if pms_folio_info.transactions: + self.compute_transactions(folio, pms_folio_info.transactions) + + 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 + """ + if pms_folio_info.transactions: + # If the payment issuer is the API client, the payment will come in transactions + # if not, we will have to look in the payload for the + # payment identifier configured in the OTA + for transaction in pms_folio_info.transactions: + if not transaction.journalId: + ota_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", pms_folio_info.pmsPropertyId), + ("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( + [ + ("pms_property_id", "=", pms_folio_info.pmsPropertyId), + ("agency_id", "=", pms_folio_info.agencyId), + ] + ) + # Compute amount total like the sum of the price in reservation lines + # and the sum of the price in service lines in the pms_folio_info + amount_total = 0 + for reservation in pms_folio_info.reservations: + if reservation.reservationLines: + amount_total += sum( + [ + line.price - (line.price * ((line.discount or 0.0) * 0.01)) + for line in reservation.reservationLines + if line.price + ] + ) + if reservation.services: + amount_total += sum( + [ + service.priceUnit * service.quantity + for service in reservation.services + if service.priceUnit and service.quantity + ] + ) + + # TODO: Review where to input the data to identify payments, + # as partnerRequest in the reservation doesn't seem like the best location. + if ( + ota_conf + and ota_conf.pms_api_alowed_payments + and any( + [ + reservation.partnerRequests + and ota_conf.pms_api_payment_identifier + in reservation.partnerRequests + for reservation in pms_folio_info.reservations + ] + ) + ): + journal = ota_conf.pms_api_payment_journal_id + pmsTransactionInfo = self.env.datamodels["pms.transaction.info"] + pms_folio_info.transactions = [ + pmsTransactionInfo( + journalId=journal.id, + transactionType="inbound", + amount=round(amount_total, 2), + date=fields.Date.today().strftime("%Y-%m-%d"), + reference=pms_folio_info.externalReference, + ) + ] + return pms_folio_info.transactions + + def wrapper_reservations(self, folio, info_reservations): + """ + This method is used to create or update the reservations in folio + We try to find the reservation in the folio, if it exists we update it + if not we create it + To find the reservation we compare the number of reservations and try + To return a list of ids with resevations to cancel by modification + """ + external_app = self.env.user.pms_api_client + cmds = [] + saved_reservations = folio.reservation_ids + agency = folio.agency_id + commision_percent_to_deduct = 0 + if external_app and agency and agency.commission_type == "subtract": + commision_percent_to_deduct = agency.default_commission + for info_reservation in info_reservations: + # 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 + proposed_reservation = saved_reservations.filtered( + 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 + if new_res: + vals.update({"folio_id": folio.id}) + if info_reservation.roomTypeId: + if ( + new_res + or proposed_reservation.room_type_id.id + != info_reservation.roomTypeId + ): + vals.update({"room_type_id": info_reservation.roomTypeId}) + if 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 + != datetime.strptime(info_reservation.checkout, "%Y-%m-%d").date() + ): + vals.update({"checkout": info_reservation.checkout}) + if info_reservation.pricelistId: + if ( + new_res + or proposed_reservation.pricelist_id.id + != info_reservation.pricelistId + ): + vals.update({"pricelist_id": info_reservation.pricelistId}) + if info_reservation.boardServiceId: + board_service_id = self.get_board_service_room_type_id( + info_reservation.boardServiceId, + info_reservation.roomTypeId, + folio.pms_property_id.id, + ) + if ( + new_res + or proposed_reservation.board_service_room_id.id != board_service_id + ): + vals.update({"board_service_room_id": board_service_id}) + if info_reservation.preferredRoomId: + if ( + new_res + or proposed_reservation.preferred_room_id.id + != info_reservation.preferredRoomId + ): + vals.update({"preferred_room_id": info_reservation.preferredRoomId}) + if info_reservation.partnerRequests: + if ( + new_res + or proposed_reservation.partner_requests + != info_reservation.partnerRequests + ): + vals.update({"partner_requests": info_reservation.partnerRequests}) + if info_reservation.adults: + if new_res or proposed_reservation.adults != info_reservation.adults: + vals.update({"adults": info_reservation.adults}) + if info_reservation.children: + if ( + new_res + or proposed_reservation.children != info_reservation.children + ): + vals.update({"children": info_reservation.children}) + 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 info_reservation.boardServiceId: + board = self.env["pms.board.service.room.type"].browse( + self.get_board_service_room_type_id( + info_reservation.boardServiceId, + info_reservation.roomTypeId, + folio.pms_property_id.id, + ) + ) + if info_reservation.adults: + board_day_price += ( + sum( + board.board_service_line_ids.with_context( + property=folio.pms_property_id.id + ) + .filtered(lambda l: l.adults) + .mapped("amount") + ) + * info_reservation.adults + ) + if info_reservation.children: + board_day_price += ( + sum( + board.board_service_line_ids.with_context( + property=folio.pms_property_id.id + ) + .filtered(lambda l: l.children) + .mapped("amount") + ) + * info_reservation.children + ) + reservation_lines_cmds = self.wrapper_reservation_lines( + reservation=info_reservation, + board_day_price=board_day_price, + proposed_reservation=proposed_reservation, + commission_percent_to_deduct=commision_percent_to_deduct, + ) + if reservation_lines_cmds: + vals.update({"reservation_line_ids": reservation_lines_cmds}) + if info_reservation.services: + reservation_services_cmds = self.wrapper_reservation_services( + info_services=info_reservation.services, + services=proposed_reservation.service_ids + if proposed_reservation + else False, + ) + if reservation_services_cmds: + vals.update({"service_ids": reservation_services_cmds}) + if not vals: + continue + elif new_res: + cmds.append((0, False, vals)) + else: + cmds.append((1, proposed_reservation.id, vals)) + return cmds + + def wrapper_reservation_lines( + self, + reservation, + board_day_price=0, + proposed_reservation=False, + commission_percent_to_deduct=0, + ): + cmds = [] + for line in reservation.reservationLines: + 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() + ) + line.price -= commission_percent_to_deduct * proposed_line.price / 100 + 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, + }, + ) + ) + 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): + cmds = [] + for info_service in info_services: + if services: + service_id = services.filtered( + lambda s: s.product_id.id == info_service.productId + ) + if service_id: + service_id = service_id[0] + services -= service_id + else: + service_id = False + + cmds.append( + ( + 0, + False, + { + "product_id": info_service.productId, + "product_qty": info_service.quantity, + "discount": info_service.discount or 0, + }, + ) + ) + return cmds + + def force_api_update_avail( + self, pms_property_id, room_type_ids, date_from, date_to + ): + """ + This method is used to force the update of the availability + of the given room types in the given dates + 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"] + .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"].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' + filter_room_type_id=room_type_id, + pms_property_codes=[pms_property.pms_property_code], + ) + + # INTERNAL METHODS FOR PUBLIC ENDPOINT + def _get_folio_or_404(self, folio_id): + folio_record = self.env["pms.folio"].sudo().browse(folio_id) + if not folio_record.exists(): + raise MissingError(_("Folio not found")) + return folio_record + + def _check_folio_access(self, folio_record, token): + try: + CustomerPortal._document_check_access( + self, "pms.folio", folio_record.id, access_token=token + ) + except AccessError: + raise MissingError(_("Folio not found")) + + def _generate_payment_link(self, folio_record): + wizard_payment_link = ( + self.env["payment.link.wizard"] + .with_context( + active_id=folio_record.id, + active_model="pms.folio", + ) + .sudo() + .create({}) + ) + wizard_payment_link._generate_link() + return wizard_payment_link.link + + def _get_folio_reservations(self, folio_record): + reservations = [] + for reservation in folio_record.reservation_ids.filtered( + lambda x: x.state != "cancel" and x.overnight_room + ): + reservation_checkin_partner_names = [ + checkin_partner.firstname + for checkin_partner in reservation.checkin_partner_ids + if all( + getattr(checkin_partner, field) + for field in self.env[ + "pms.checkin.partner" + ]._checkin_mandatory_fields() + ) + ] + reservations.append( + self.env.datamodels["pms.reservation.public.info"]( + id=reservation.id, + roomTypeName=reservation.room_type_id.name, + checkinNamesCompleted=reservation_checkin_partner_names, + accessToken=self._get_reservation_access_token(reservation), + nights=reservation.nights, + checkin=datetime.combine( + reservation.checkin, datetime.min.time() + ).isoformat(), + checkout=datetime.combine( + reservation.checkout, datetime.min.time() + ).isoformat(), + adults=reservation.adults, + children=reservation.children, + ) + ) + return reservations + + def _get_reservation_access_token(self, reservation): + if not reservation.access_token: + reservation.access_token = reservation._portal_ensure_token() + return reservation.access_token + + def _build_room_types_description(self, folio_record): + room_type_counts = {} + for name in folio_record.reservation_ids.filtered( + lambda x: x.state != "cancel" + ).mapped("room_type_id.name"): + room_type_counts[name] = room_type_counts.get(name, 0) + 1 + + return ", ".join(f"{count} {name}" for name, count in room_type_counts.items()) + + def _get_folio_portal_link(self, folio_record): + return ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url") + + folio_record.get_portal_url() + if folio_record.get_portal_url() + else "" + ) + + def _get_folio_checkin_names_completed(self, folio_record): + return [ + checkin_partner.firstname + for reservation in folio_record.reservation_ids + for checkin_partner in reservation.checkin_partner_ids + if all( + getattr(checkin_partner, field) + for field in self.env["pms.checkin.partner"]._checkin_mandatory_fields() + ) + ] + + # PUBLIC ENDPOINTS + @restapi.method( + [ + ( + "//precheckin/", + "GET", + ) + ], + output_param=Datamodel("pms.folio.public.info", is_list=False), + auth="public", + ) + def get_folio_public_info(self, folio_id, token): + folio_record = self._get_folio_or_404(folio_id) + + self._check_folio_access(folio_record, token) + folio_payment_link = self._generate_payment_link(folio_record) + reservations = self._get_folio_reservations(folio_record) + folio_room_types_description_result = self._build_room_types_description( + folio_record + ) + folio_portal_link = self._get_folio_portal_link(folio_record) + ine_category = ( + f"{folio_record.pms_property_id.ine_category_id.category} ({folio_record.pms_property_id.ine_category_id.type})" + if folio_record.pms_property_id.ine_category_id + else "" + ) + + return self.env.datamodels["pms.folio.public.info"]( + pmsPropertyName=folio_record.pms_property_id.name, + pmsPropertyStreet=folio_record.pms_property_id.street, + pmsPropertyCity=folio_record.pms_property_id.city, + pmsPropertyState=folio_record.pms_property_id.state_id.name + if folio_record.pms_property_id.state_id + else "", + pmsPropertyPhoneNumber=folio_record.pms_property_id.phone, + pmsPropertyLogo=url_image_pms_api_rest( + "pms.property", folio_record.pms_property_id.id, "logo" + ), + pmsPropertyImage=url_image_pms_api_rest( + "pms.property", + folio_record.pms_property_id.id, + "hotel_image_pms_api_rest", + ), + pmsPropertyIsOCRAvailable=bool( + folio_record.pms_property_id.ocr_checkin_supplier + ), + pmsPropertyPrivacyPolicy=folio_record.pms_property_id.privacy_policy or "", + pmsPropertyZip=folio_record.pms_property_id.zip, + pmsPropertyIneCategory=ine_category, + folioPartnerName=folio_record.partner_name, + folioReference=folio_record.name, + folioRoomTypesDescription=folio_room_types_description_result.rstrip(", "), + folioPendingAmount=folio_record.pending_amount, + folioPaymentLink=folio_payment_link + if folio_payment_link else "", + folioPortalLink=folio_portal_link, + folioNumCheckins=sum( + len(r.checkin_partner_ids) for r in folio_record.reservation_ids + ), + folioCheckinNamesCompleted=self._get_folio_checkin_names_completed( + folio_record + ), + reservations=reservations, + ) diff --git a/pms_api_rest/services/pms_id_category_service.py b/pms_api_rest/services/pms_id_category_service.py new file mode 100644 index 0000000000..238a8673da --- /dev/null +++ b/pms_api_rest/services/pms_id_category_service.py @@ -0,0 +1,41 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsIdCategoryService(Component): + _inherit = "base.rest.service" + _name = "pms.id.category.service" + _usage = "id-categories" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.id.category.info", is_list=True), + auth="public", + ) + def get_id_categories(self): + result_id_categories = [] + PmsIdCategoryInfo = self.env.datamodels["pms.id.category.info"] + for id_category in ( + self.env["res.partner.id_category"] + .with_context(lang=self.env.user.lang) + .sudo() + .search([], order="priority asc") + ): + result_id_categories.append( + PmsIdCategoryInfo( + id=id_category.id, + documentType=id_category.name, + code=id_category.code, + countryIds=id_category.country_ids.mapped("id"), + ) + ) + return result_id_categories diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py new file mode 100644 index 0000000000..cf14f2d41b --- /dev/null +++ b/pms_api_rest/services/pms_invoice_service.py @@ -0,0 +1,642 @@ +import base64 +from datetime import datetime + +import werkzeug.exceptions + +from odoo import _, fields +from odoo.exceptions import MissingError, UserError +from odoo.osv import expression + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsInvoiceService(Component): + _inherit = "base.rest.service" + _name = "pms.invoice.service" + _usage = "invoices" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.invoice.search.param"), + output_param=Datamodel("pms.invoice.info"), + auth="jwt_api_pms", + ) + def get_invoices(self, pms_invoice_search_param): + order_by_param = False + result_invoices = [] + + domain = [] + domain_fields = [ + ("state", "in", ("draft", "posted")), + ("move_type", "in", ("out_invoice", "out_refund")), + ("folio_ids", "!=", False), + ] + domain_filter = list() + + if pms_invoice_search_param.originAgencyId: + domain_fields.append( + ("origin_agency_id", "=", pms_invoice_search_param.originAgencyId), + ) + if pms_invoice_search_param.pmsPropertyId: + domain_fields.append( + ("pms_property_id", "=", pms_invoice_search_param.pmsPropertyId) + ) + if pms_invoice_search_param.paymentState == "paid": + domain_fields.append( + ("payment_state", "in", ("paid", "reversed", "invoicing_legacy")) + ) + elif pms_invoice_search_param.paymentState == "not_paid": + domain_fields.append(("payment_state", "in", ("not_paid", "in_payment"))) + elif pms_invoice_search_param.paymentState == "partial": + domain_fields.append( + ("payment_state", "=", pms_invoice_search_param.paymentState) + ) + if pms_invoice_search_param.dateStart and pms_invoice_search_param.dateEnd: + date_from = fields.Date.from_string(pms_invoice_search_param.dateStart) + date_to = fields.Date.from_string(pms_invoice_search_param.dateEnd) + domain_fields.extend( + [ + ("invoice_date", ">=", date_from), + ("invoice_date", "<=", date_to), + ] + ) + if pms_invoice_search_param.filter: + for search in pms_invoice_search_param.filter.split(" "): + subdomains = [ + [("name", "ilike", search)], + [("partner_id.display_name", "ilike", search)], + [("partner_id.vat", "ilike", search)], + [ + ( + "partner_id.aeat_identification", + "ilike", + search, + ) + ], + [("folio_ids.name", "ilike", search)], + ] + domain_filter.append(expression.OR(subdomains)) + + if domain_filter: + domain = expression.AND([domain_fields, domain_filter[0]]) + else: + domain = domain_fields + PmsInvoiceResults = self.env.datamodels["pms.invoice.results"] + PmsInvoiceInfo = self.env.datamodels["pms.invoice.info"] + PmsInvoiceLineInfo = self.env.datamodels["pms.invoice.line.info"] + total_invoices = self.env["account.move"].search_count(domain) + if pms_invoice_search_param.orderBy: + order_by_param = self._get_mapped_order_by_field( + pms_invoice_search_param.orderBy + ) + (" desc" if pms_invoice_search_param.orderDesc else " asc") + amount_total = sum( + self.env["account.move"] + .search( + domain, + order=order_by_param if order_by_param else False, + limit=pms_invoice_search_param.limit, + offset=pms_invoice_search_param.offset, + ) + .mapped("amount_total") + ) + for invoice in self.env["account.move"].search( + domain, + order=order_by_param if order_by_param else False, + limit=pms_invoice_search_param.limit, + offset=pms_invoice_search_param.offset, + ): + + move_lines = [] + + for move_line in invoice.invoice_line_ids: + move_lines.append( + PmsInvoiceLineInfo( + id=move_line.id, + name=move_line.name if move_line.name else None, + quantity=move_line.quantity if move_line.quantity else None, + priceUnit=move_line.price_unit + if move_line.price_unit + else None, + total=move_line.price_total if move_line.price_total else None, + discount=move_line.discount if move_line.discount else None, + displayType=move_line.display_type + if move_line.display_type + else None, + saleLineId=move_line.folio_line_ids[0] + if move_line.folio_line_ids + else None, + isDownPayment=move_line.move_id._is_downpayment(), + ) + ) + invoice_date = ( + datetime.combine(invoice.invoice_date, datetime.min.time()).isoformat() + if invoice.invoice_date + else datetime.combine( + invoice.invoice_date_due, datetime.min.time() + ).isoformat() + if invoice.invoice_date_due + else None + ) + invoice_url = ( + invoice.get_proforma_portal_url() + if invoice.state == "draft" + else invoice.get_portal_url() + ) + portal_url = ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url") + + invoice_url + ) + result_invoices.append( + PmsInvoiceInfo( + id=invoice.id if invoice.id else None, + name=invoice.name if invoice.name else None, + amount=round(invoice.amount_total, 2) + if invoice.amount_total + else None, + date=invoice_date, + state=invoice.state if invoice.state else None, + paymentState=invoice.payment_state + if invoice.payment_state + else None, + partnerName=invoice.partner_id.name + if invoice.partner_id.name + else None, + partnerId=invoice.partner_id.id if invoice.partner_id.id else None, + moveLines=move_lines if len(move_lines) > 0 else None, + folioId=invoice.folio_ids[0] if invoice.folio_ids else None, + portalUrl=portal_url, + moveType=invoice.move_type, + isReversed=invoice.payment_state == "reversed", + isDownPaymentInvoice=invoice._is_downpayment(), + isSimplifiedInvoice=invoice.journal_id.is_simplified_invoice, + originAgencyId=invoice.origin_agency_id.id + if invoice.origin_agency_id + else None, + ref=invoice.ref if invoice.ref else None, + narration=invoice.narration if invoice.narration else None, + pmsPropertyId=invoice.pms_property_id + if invoice.pms_property_id + else None, + ) + ) + return PmsInvoiceResults( + invoices=result_invoices, + total=round(amount_total, 2), + totalInvoices=total_invoices, + ) + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.invoice.info"), + auth="jwt_api_pms", + ) + # flake8: noqa: C901 + def update_invoice(self, invoice_id, pms_invoice_info): + invoice = self.env["account.move"].browse(invoice_id) + if invoice.move_type in ["in_refund", "out_refund"]: + raise UserError(_("You can't update a refund invoice")) + if invoice.payment_state == "reversed": + raise UserError(_("You can't update a reversed invoice")) + invoice._check_fiscalyear_lock_date() + new_vals = {} + if ( + pms_invoice_info.partnerId + and pms_invoice_info.partnerId != invoice.partner_id.id + ): + new_vals["partner_id"] = pms_invoice_info.partnerId + + if pms_invoice_info.date: + invoice_date_info = fields.Date.from_string(pms_invoice_info.date) + if invoice_date_info != invoice.invoice_date: + # If the invoice is in draft state and the date is less than today, + if invoice_date_info < fields.Date.today() and not invoice.invoice_date: + new_vals["invoice_date"] = fields.Date.today() + else: + new_vals["invoice_date"] = invoice_date_info + + # If invoice lines are updated, we expect that all lines will be + # send to service, the lines that are not sent we assume that + # they have been eliminated + if pms_invoice_info.moveLines is not None: + cmd_invoice_lines = self._get_invoice_lines_commands( + invoice, pms_invoice_info + ) + if cmd_invoice_lines: + new_vals["invoice_line_ids"] = cmd_invoice_lines + # Update Journal by partner if necessary (simplified invoice -> normal invoice) + if pms_invoice_info.isSimplifiedInvoice is not None: + if ( + invoice.journal_id.is_simplified_invoice + != pms_invoice_info.isSimplifiedInvoice + ): + new_vals["journal_id"] = ( + invoice.pms_property_id.journal_simplified_invoice_id.id + if pms_invoice_info.isSimplifiedInvoice + else invoice.pms_property_id.journal_normal_invoice_id.id + ) + new_invoice = False + if new_vals: + # Update Invoice + # When modifying an invoice, depending on the company's configuration, + # and the invoice state it will be modified directly or a reverse + # of the current invoice will be created to later create a new one + # with the updated data. + # TODO: to create core pms correct_invoice_policy field + # if invoice.state != "draft" and company.corrective_invoice_policy == "strict": + if invoice.state == "posted" and self.check_blocked_fields( + invoice, new_vals + ): + # invoice create refund + new_invoice = invoice.copy() + cmd_new_invoice_lines = [] + for item in cmd_invoice_lines: + # susbstituted in new_vals reversed invoice line id by new invoice line id + if item[0] == 0: + cmd_new_invoice_lines.append(item) + else: + folio_line_ids = self.env["folio.sale.line"].browse( + self.env["account.move.line"] + .browse(item[1]) + .folio_line_ids.ids + ) + new_id = new_invoice.invoice_line_ids.filtered( + lambda l: l.folio_line_ids == folio_line_ids + ).id + if item[0] == 2: + # delete + cmd_new_invoice_lines.append((2, new_id)) + else: + # update + cmd_new_invoice_lines.append((1, new_id, item[2])) + if cmd_new_invoice_lines: + new_vals["invoice_line_ids"] = cmd_new_invoice_lines + default_values_list = [ + { + "ref": _( + f'Reversal of: {move.name + (" - " + move.ref if move.ref else "")}' + ), + } + for move in invoice + ] + invoice._reverse_moves(default_values_list, cancel=True) + new_invoice.write(new_vals) + new_invoice.sudo().action_post() + else: + new_invoice = self._direct_move_update(invoice, new_vals) + invoice_to_update = new_invoice or invoice + # Clean sections without lines + folio_lines_invoiced = invoice_to_update.invoice_line_ids.folio_line_ids + for folio_line in folio_lines_invoiced.filtered( + lambda l: l.display_type == "line_section" + ): + if ( + not folio_line.id + in folio_lines_invoiced.filtered( + lambda l: l.display_type != "line_section" + ).section_id.ids + ): + folio_line.invoice_lines.filtered( + lambda l: l.move_id == invoice_to_update + ).unlink() + + if pms_invoice_info.narration is not None: + invoice_to_update.write({"narration": pms_invoice_info.narration}) + if invoice_to_update.state == "draft" and pms_invoice_info.state == "confirm": + invoice_to_update.action_post() + if ( + invoice_to_update.state == "draft" + and not invoice_to_update.invoice_line_ids + ): + invoice_to_update.unlink() + return invoice_to_update.id or None + + @restapi.method( + [ + ( + [ + "/", + ], + "DELETE", + ) + ], + auth="jwt_api_pms", + ) + def delete_invoice(self, invoice_id): + invoice = self.env["account.move"].browse(invoice_id) + if invoice: + if invoice.state != "draft": + raise UserError(_("Only draft invoices can be deleted")) + invoice.unlink() + else: + raise MissingError(_("Invoice not found")) + + def _direct_move_update(self, invoice, new_vals): + previus_state = invoice.state + if previus_state == "posted": + invoice.button_draft() + if new_vals: + updated_invoice_lines_name = False + # REVIEW: If invoice lines are updated (lines that already existed), + # the _move_autocomplete_invoice_lines_write called accout.move write + # method overwrite the move_lines dict and we lost the new name values, + # so, we need to save and rewrite it. (core odoo methods) + + # 1- save send invoice line name values: + if new_vals.get("invoice_line_ids"): + updated_invoice_lines_name = { + line[1]: line[2]["name"] + for line in new_vals["invoice_line_ids"] + if line[0] == 1 and "name" in line[2] + } + # _move_autocomplete_invoice_lines_write overwrite invoice line name values + # so, we need to save and rewrite it. in all line that are not updated or deleted + for line in invoice.invoice_line_ids.filtered( + lambda l: l.id not in updated_invoice_lines_name + if updated_invoice_lines_name + else [] + and l.id + not in [ + line[1] for line in new_vals["invoice_line_ids"] if line[0] == 2 + ] + ): + updated_invoice_lines_name[line.id] = line.name + # 2- update invoice + invoice.write(new_vals) + # 3- rewrite invoice line name values: + if updated_invoice_lines_name: + for item in updated_invoice_lines_name: + invoice.invoice_line_ids.filtered(lambda l: l.id == item).write( + {"name": updated_invoice_lines_name[item]} + ) + # 4- Avoid set number invoice in draft invoices + if previus_state == "draft": + invoice.write({"name": "/"}) + if previus_state == "posted": + invoice.action_post() + return invoice + + def check_blocked_fields(self, invoice, new_vals): + # Check partner and amounts + if ( + new_vals.get("partner_id") + and new_vals.get("partner_id") != invoice.partner_id.id + ): + return True + if new_vals.get("invoice_line_ids"): + for line in new_vals["invoice_line_ids"]: + if line[0] == 2: + move_line = self.env["account.move.line"].browse(line[1]) + if not move_line.display_type: + return True + if line[0] == 1: + move_line = self.env["account.move.line"].browse(line[1]) + if "quantity" in line[2] and move_line.quantity != line[2].get( + "quantity" + ): + return True + if line[0] == 0 and not line[2].get("display_type"): + return True + return False + + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.invoice.info"), + auth="jwt_api_pms", + ) + def create_invoice(self, pms_invoice_info): + if pms_invoice_info.originDownPaymentId: + payment = self.env["account.payment"].browse( + pms_invoice_info.originDownPaymentId + ) + self.env["account.payment"]._create_downpayment_invoice( + payment=payment, + partner_id=pms_invoice_info.partnerId, + ) + + @restapi.method( + [ + ( + [ + "//mail", + ], + "GET", + ) + ], + output_param=Datamodel("pms.mail.info", is_list=False), + auth="jwt_api_pms", + ) + def get_invoice_mail(self, invoice_id): + invoice = self.env["account.move"].browse(invoice_id) + compose_vals = { + "template_id": self.env.ref("account.email_template_edi_invoice").id, + "model": "account.move", + "res_ids": invoice.id, + } + values = self.env["mail.compose.message"].generate_email_for_composer( + template_id=compose_vals["template_id"], + res_ids=compose_vals["res_ids"], + fields=["subject", "body_html"], + ) + PmsMailInfo = self.env.datamodels["pms.mail.info"] + return PmsMailInfo( + bodyMail=values["body"], + subject=values["subject"], + ) + + @restapi.method( + [ + ( + [ + "/send-mail-print-invoices", + ], + "POST", + ) + ], + input_param=Datamodel("pms.account.send.search.param", is_list=False), + output_param=Datamodel("pms.report", is_list=False), + auth="jwt_api_pms", + ) + def send_mail_print_invoices(self, account_send_search_param): + invoices = self.env["account.move"].browse(account_send_search_param.invoiceIds) + template = self.env.ref( + "account.email_template_edi_invoice", raise_if_not_found=False + ) + PmsResponse = self.env.datamodels["pms.report"] + base64_encoded_str = None + + for invoice in invoices: + if account_send_search_param.isEmail: + email_values = { + "email_from": invoice.pms_property_id.email + if invoice.pms_property_id.email + else False, + "auto_delete": False, + "partner_ids": account_send_search_param.partnerIds + if account_send_search_param.partnerIds + else False, + "recipient_ids": account_send_search_param.partnerIds + if account_send_search_param.partnerIds + else False, + "email_to": account_send_search_param.emailAddresses, + } + template.send_mail( + invoice.id, force_send=True, email_values=email_values + ) + if account_send_search_param.isPrint: + invoice_report = self.env.ref("account.account_invoices") + pdf_content, _ = invoice_report._render_qweb_pdf(invoices.ids) + base64_encoded_str = base64.b64encode(pdf_content) + return PmsResponse(binary=base64_encoded_str) + + @restapi.method( + [ + ( + [ + "//send-mail", + ], + "POST", + ) + ], + input_param=Datamodel("pms.mail.info"), + auth="jwt_api_pms", + ) + def send_invoice_mail(self, invoice_id, pms_mail_info): + invoice = self.env["account.move"].browse(invoice_id) + recipients = pms_mail_info.emailAddresses + template = self.env.ref( + "account.email_template_edi_invoice", raise_if_not_found=False + ) + email_values = { + "email_to": ",".join(recipients) if recipients else False, + "email_from": invoice.pms_property_id.email + if invoice.pms_property_id.email + else False, + "subject": pms_mail_info.subject, + "body_html": pms_mail_info.bodyMail, + "partner_ids": pms_mail_info.partnerIds + if pms_mail_info.partnerIds + else False, + "recipient_ids": pms_mail_info.partnerIds + if pms_mail_info.partnerIds + else False, + "auto_delete": False, + } + template.send_mail(invoice.id, force_send=True, email_values=email_values) + invoice.write( + { + "is_move_sent": True, + } + ) + return True + + def _get_invoice_lines_commands(self, invoice, pms_invoice_info): + cmd_invoice_lines = [] + for line in invoice.invoice_line_ids: + line_info = [ + item for item in pms_invoice_info.moveLines if item.id == line.id + ] + if line_info: + line_info = line_info[0] + line_values = {} + if line_info.name: + line_values["name"] = line_info.name + if line_info.quantity and line_info.quantity != line.quantity: + line_values["quantity"] = line_info.quantity + if line_values: + cmd_invoice_lines.append((1, line.id, line_values)) + elif not line.display_type: + cmd_invoice_lines.append((2, line.id)) + # Get the new lines to add in invoice + newInvoiceLinesInfo = list( + filter(lambda item: not item.id, pms_invoice_info.moveLines) + ) + if newInvoiceLinesInfo: + partner = ( + self.env["res.partner"].browse(pms_invoice_info.partnerId) + if pms_invoice_info.partnerId + else invoice.partner_id + ) + folios = self.env["pms.folio"].browse( + list( + { + self.env["folio.sale.line"].browse(line.saleLineId).folio_id.id + for line in list( + filter( + lambda item: item.name, + pms_invoice_info.moveLines, + ) + ) + } + ) + ) + lines_to_invoice = { + newInvoiceLinesInfo[i].saleLineId: newInvoiceLinesInfo[i].quantity + for i in range(0, len(newInvoiceLinesInfo)) + } + # Add sections to invoice lines + new_section_ids = ( + self.env["folio.sale.line"] + .browse([line.saleLineId for line in newInvoiceLinesInfo]) + .filtered( + lambda l: l.section_id.id + not in invoice.invoice_line_ids.mapped("folio_line_ids.id") + ) + .mapped("section_id.id") + ) + if new_section_ids: + lines_to_invoice.update( + {section_id: 0 for section_id in new_section_ids} + ) + new_invoice_lines = [ + item["invoice_line_ids"] + for item in folios.get_invoice_vals_list( + lines_to_invoice=lines_to_invoice, + partner_invoice_id=partner.id, + ) + ][0] + # Update name of new invoice lines + for item in filter(lambda l: not l[2]["display_type"], new_invoice_lines): + item[2]["name"] = [ + line.name + for line in newInvoiceLinesInfo + if [line.saleLineId] == item[2]["folio_line_ids"][0][2] + ][0] + cmd_invoice_lines.extend(new_invoice_lines) + return cmd_invoice_lines + + def _get_mapped_order_by_field(self, field): + if field == "name": + result = "name" + elif field == "ref": + result = "ref" + elif field == "date": + result = "invoice_date" + elif field == "amount": + result = "amount_total" + else: + raise werkzeug.exceptions.MethodNotAllowed(description="Field not allowed") + return result diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py new file mode 100644 index 0000000000..782486390e --- /dev/null +++ b/pms_api_rest/services/pms_login_service.py @@ -0,0 +1,89 @@ +import time + +import werkzeug.exceptions +from jose import jwt + +from odoo import _ +from odoo.exceptions import AccessDenied + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +from ..pms_api_rest_utils import url_image_pms_api_rest + + +class PmsLoginService(Component): + _inherit = "base.rest.service" + _name = "pms.auth.service" + _usage = "login" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.api.rest.user.input", is_list=False), + output_param=Datamodel("pms.api.rest.user.output", is_list=False), + auth="public", + cors="*", + ) + def login(self, user): + user_record = ( + self.env["res.users"].sudo().search([("login", "=", user.username)]) + ) + # formula = ms_now + 24 hours + timestamp_expire_in_a_sec = int(time.time()) + 24 * 60 * 60 + + if not user_record: + raise werkzeug.exceptions.Unauthorized(_("wrong user/pass")) + try: + user_record.with_user(user_record)._check_credentials(user.password, None) + except AccessDenied: + raise werkzeug.exceptions.Unauthorized(_("wrong user/pass")) + + validator = ( + self.env["auth.jwt.validator"].sudo()._get_validator_by_name("api_pms") + ) + assert len(validator) == 1 + + PmsApiRestUserOutput = self.env.datamodels["pms.api.rest.user.output"] + + token = jwt.encode( + { + "aud": "api_pms", + "iss": "pms", + "exp": timestamp_expire_in_a_sec, + "username": user.username, + }, + key=validator.secret_key, + algorithm=validator.secret_algorithm, + ) + avail_rule_names = [] + for avail_field in user_record.availability_rule_field_ids: + avail_rule_names.append(avail_field.name) + + return PmsApiRestUserOutput( + token=token, + expirationDate=timestamp_expire_in_a_sec, + userId=user_record.id, + userName=user_record.name, + userFirstName=user_record.firstname or None, + userEmail=user_record.email or None, + userPhone=user_record.phone or None, + defaultPropertyId=user_record.pms_property_id.id, + defaultPropertyName=user_record.pms_property_id.name, + userImageBase64=user_record.partner_id.image_1024 + if user_record.partner_id.image_1024 + else None, + userImageUrl=url_image_pms_api_rest( + "res.partner", user_record.partner_id.id, "image_1024" + ), + isNewInterfaceUser=user_record.is_new_interface_app_user, + availabilityRuleFields=avail_rule_names, + ) diff --git a/pms_api_rest/services/pms_notification_service.py b/pms_api_rest/services/pms_notification_service.py new file mode 100644 index 0000000000..15eed1d8d2 --- /dev/null +++ b/pms_api_rest/services/pms_notification_service.py @@ -0,0 +1,45 @@ +import datetime + +from odoo import fields + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsNotificationService(Component): + _inherit = "base.rest.service" + _name = "pms.notification.service" + _usage = "notifications" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/reservations-to-assign", + ], + "GET", + ) + ], + input_param=Datamodel("pms.notification.search", is_list=False), + output_param=Datamodel("pms.notification.info", is_list=False), + auth="jwt_api_pms", + cors="*", + ) + def get_reservations_to_assign_notifications(self, pms_notification_search): + + num_reservation_ids_to_assign = self.env["pms.reservation"].search_count( + [ + # this domain should be the same as folio service for unassigned reservations + ("pms_property_id", "=", pms_notification_search.pmsPropertyId), + ("checkin", ">=", fields.Date.today()), + ("to_assign", "=", True), + ("state", "in", ("draft", "confirm", "arrival_delayed")), + ("reservation_type", "!=", "out"), + ], + ) + PmsNotificationInfo = self.env.datamodels["pms.notification.info"] + return PmsNotificationInfo( + numReservationsToAssign=num_reservation_ids_to_assign + ) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py new file mode 100644 index 0000000000..4dd3ab033b --- /dev/null +++ b/pms_api_rest/services/pms_partner_service.py @@ -0,0 +1,936 @@ +import re +from datetime import date, datetime, timedelta + +from odoo import _ +from odoo.exceptions import ValidationError +from odoo.osv import expression + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +_ref_vat = { + "al": "J91402501L", + "ar": "200-5536168-2 or 20055361682", + "at": "U12345675", + "au": "83 914 571 673", + "be": "0477472701", + "bg": "1234567892", + "ch": "CHE-123.456.788 TVA or CHE-123.456.788 MWST or CHE-123.456.788 IVA", + "cl": "76086428-5", + "co": "213123432-1 or 213.123.432-1", + "cy": "10259033P", + "cz": "12345679", + "de": "123456788", + "dk": "12345674", + "do": "1-01-85004-3 or 101850043", + "ec": "1792060346-001", + "ee": "123456780", + "el": "12345670", + "es": "12345674A", + "fi": "12345671", + "fr": "23334175221", + "gb": "123456782 or 123456782", + "gr": "12345670", + "hu": "12345676", + "hr": "01234567896", + "ie": "1234567FA", + "in": "12AAAAA1234AAZA", + "is": "062199", + "it": "12345670017", + "lt": "123456715", + "lu": "12345613", + "lv": "41234567891", + "mc": "53000004605", + "mt": "12345634", + "mx": "GODE561231GR8", + "nl": "123456782B90", + "no": "123456785", + "pe": "10XXXXXXXXY or 20XXXXXXXXY or 15XXXXXXXXY or 16XXXXXXXXY or 17XXXXXXXXY", + "ph": "123-456-789-123", + "pl": "1234567883", + "pt": "123456789", + "ro": "1234567897", + "rs": "101134702", + "ru": "123456789047", + "se": "123456789701", + "si": "12345679", + "sk": "2022749619", + "sm": "24165", + "tr": "1234567890 (VERGINO) or 17291716060 (TCKIMLIKNO)", + "ve": "V-12345678-1, V123456781, V-12.345.678-1", + "xi": "123456782", +} + + +class PmsPartnerService(Component): + _inherit = "base.rest.service" + _name = "pms.partner.service" + _usage = "partners" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.partner.search.param", is_list=False), + output_param=Datamodel("pms.partner.results", is_list=False), + auth="jwt_api_pms", + ) + def get_partners(self, pms_partner_search_params): + result_partners = [] + domain = [] + + if pms_partner_search_params.housedNow: + partners_housed_now = ( + self.env["pms.checkin.partner"] + .search([("state", "=", "onboard")]) + .mapped("partner_id") + ) + domain.append(("id", "in", partners_housed_now.ids)) + if pms_partner_search_params.housedLastWeek: + today = date.today() + last_week_day = today - timedelta(days=7) + partners_housed_last_week = ( + self.env["pms.checkin.partner"] + .search( + [ + "|", + "&", + ("checkin", ">=", last_week_day), + ("checkin", "<=", today), + "|", + ("checkout", ">=", last_week_day), + ("checkout", "<=", today), + "|", + "&", + ("checkin", "<=", last_week_day), + ("checkout", "<", today), + "&", + ("checkin", ">=", last_week_day), + ("checkout", ">", today), + "|", + ("checkin", "<", last_week_day), + ("checkout", ">", today), + ] + ) + .mapped("partner_id") + ) + domain.append(("id", "in", partners_housed_last_week.ids)) + if pms_partner_search_params.housedLastMonth: + today = date.today() + last_month_day = today - timedelta(days=30) + partners_housed_last_month = ( + self.env["pms.checkin.partner"] + .search( + [ + "|", + "&", + ("checkin", ">=", last_month_day), + ("checkin", "<=", today), + "|", + ("checkout", ">=", last_month_day), + ("checkout", "<=", today), + "|", + "&", + ("checkin", "<=", last_month_day), + ("checkout", "<", today), + "&", + ("checkin", ">=", last_month_day), + ("checkout", ">", today), + "|", + ("checkin", "<", last_month_day), + ("checkout", ">", today), + ] + ) + .mapped("partner_id") + ) + domain.append(("id", "in", partners_housed_last_month.ids)) + if ( + pms_partner_search_params.filterByType + and pms_partner_search_params.filterByType != "all" + ): + if pms_partner_search_params.filterByType == "company": + domain.append(("is_company", "=", True)) + elif pms_partner_search_params.filterByType == "agency": + domain.append(("is_agency", "=", True)) + elif pms_partner_search_params.filterByType == "individual": + domain.append(("is_company", "=", False)) + domain.append(("is_agency", "=", False)) + + if pms_partner_search_params.filter: + subdomains = [ + [("vat", "ilike", pms_partner_search_params.filter)], + [ + ( + "aeat_identification", + "ilike", + pms_partner_search_params.filter, + ) + ], + [("display_name", "ilike", pms_partner_search_params.filter)], + ] + if "@" in pms_partner_search_params.filter: + subdomains.append( + [("email", "ilike", pms_partner_search_params.filter)] + ) + domain_partner_search_field = expression.OR(subdomains) + domain = expression.AND([domain, domain_partner_search_field]) + PmsPartnerResults = self.env.datamodels["pms.partner.results"] + PmsPartnerInfo = self.env.datamodels["pms.partner.info"] + total_partners = self.env["res.partner"].search_count(domain) + + for partner in self.env["res.partner"].search( + domain, + limit=pms_partner_search_params.limit, + offset=pms_partner_search_params.offset, + ): + checkouts = ( + self.env["pms.checkin.partner"] + .search([("partner_id.id", "=", partner.id)]) + .filtered(lambda x: x.checkout) + .mapped("checkout") + ) + doc_record = False + document_number = False + document_type = False + document_support_number = False + document_country_id = False + document_expedition_date = False + if partner.id_numbers: + doc_record = partner.id_numbers[0] + if doc_record: + if doc_record.name: + document_number = doc_record.name + if doc_record.category_id: + document_type = doc_record.category_id.id + if doc_record.support_number: + document_support_number = doc_record.support_number + if doc_record.country_id: + document_country_id = doc_record.country_id.id + if doc_record.valid_from: + document_expedition_date = datetime.combine( + doc_record.valid_from, datetime.min.time() + ).isoformat() + result_partners.append( + PmsPartnerInfo( + id=partner.id, + name=partner.name if partner.name else None, + firstname=partner.firstname if partner.firstname else None, + lastname=partner.lastname if partner.lastname else None, + lastname2=partner.lastname2 if partner.lastname2 else None, + email=partner.email if partner.email else None, + phone=partner.phone if partner.phone else None, + gender=partner.gender if partner.gender else None, + birthdate=datetime.combine( + partner.birthdate_date, datetime.min.time() + ).isoformat() + if partner.birthdate_date + else None, + age=partner.age if partner.age else None, + mobile=partner.mobile if partner.mobile else None, + residenceStreet=partner.residence_street + if partner.residence_street + else None, + residenceStreet2=partner.residence_street2 + if partner.residence_street2 + else None, + residenceZip=partner.residence_zip + if partner.residence_zip + else None, + residenceCity=partner.residence_city + if partner.residence_city + else None, + nationality=partner.nationality_id.id + if partner.nationality_id + else None, + residenceStateId=partner.residence_state_id.id + if partner.residence_state_id + else None, + street=partner.street if partner.street else None, + street2=partner.street2 if partner.street2 else None, + zip=partner.zip if partner.zip else None, + countryId=partner.country_id.id if partner.country_id else None, + stateId=partner.state_id.id if partner.state_id else None, + city=partner.city if partner.city else None, + isAgency=partner.is_agency, + isCompany=partner.is_company, + residenceCountryId=partner.residence_country_id.id + if partner.residence_country_id + else None, + documentNumber=document_number if document_number else None, + documentType=document_type if document_type else None, + documentSupportNumber=document_support_number + if document_support_number + else None, + documentCountryId=document_country_id + if document_country_id + else None, + vatNumber=partner.vat + if partner.vat + else partner.aeat_identification + if partner.aeat_identification + else None, + vatDocumentType="02" + if partner.vat + else partner.aeat_identification_type + if partner.aeat_identification_type + else None, + documentExpeditionDate=document_expedition_date + if document_expedition_date + else None, + comment=partner.comment if partner.comment else None, + language=partner.lang if partner.lang else None, + userId=partner.user_id if partner.user_id else None, + paymentTerms=partner.property_payment_term_id + if partner.property_payment_term_id + else None, + pricelistId=partner.property_product_pricelist + if partner.property_product_pricelist + else None, + salesReference=partner.ref if partner.ref else None, + saleChannelId=partner.sale_channel_id + if partner.sale_channel_id + else None, + commission=partner.default_commission + if partner.default_commission + else None, + invoicingPolicy=partner.invoicing_policy + if partner.invoicing_policy + else None, + daysAutoInvoice=partner.margin_days_autoinvoice + if partner.margin_days_autoinvoice + else None, + invoicingMonthDay=partner.invoicing_month_day + if partner.invoicing_month_day + else None, + invoiceToAgency=partner.invoice_to_agency + if partner.invoice_to_agency + else None, + tagIds=partner.category_id.ids if partner.category_id else [], + lastStay=max(checkouts).strftime("%d/%m/%Y") if checkouts else "", + ) + ) + return PmsPartnerResults(partners=result_partners, total=total_partners) + + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.partner.info", is_list=False), + auth="jwt_api_pms", + ) + def create_partner(self, partner_info): + vals = self.mapping_partner_values(partner_info) + partner = self.env["res.partner"].create(vals) + return partner.id + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.partner.info", is_list=False), + auth="jwt_api_pms", + ) + def write_partner(self, partner_id, partner_info): + partner = self.env["res.partner"].browse(partner_id) + if partner: + partner.write(self.mapping_partner_values(partner_info)) + + @restapi.method( + [ + ( + [ + "//payments", + ], + "GET", + ) + ], + output_param=Datamodel("pms.transaction.info", is_list=True), + auth="jwt_api_pms", + ) + def get_partner_payments(self, partner_id): + partnerPayments = self.env["account.payment"].search( + [("partner_id", "=", partner_id)] + ) + PmsTransactiontInfo = self.env.datamodels["pms.transaction.info"] + payments = [] + for payment in partnerPayments: + destination_journal_id = False + if payment.is_internal_transfer: + destination_journal_id = ( + payment.pms_api_counterpart_payment_id.journal_id.id + ) + payments.append( + PmsTransactiontInfo( + id=payment.id, + name=payment.name if payment.name else None, + amount=payment.amount, + journalId=payment.journal_id.id if payment.journal_id else None, + destinationJournalId=destination_journal_id + if destination_journal_id + else None, + date=datetime.combine( + payment.date, datetime.min.time() + ).isoformat(), + folioId=payment.folio_ids[0].id if payment.folio_ids else None, + partnerId=payment.partner_id.id if payment.partner_id else None, + partnerName=payment.partner_id.name if payment.partner_id else None, + reference=payment.ref if payment.ref else None, + createUid=payment.create_uid.id if payment.create_uid else None, + transactionType=payment.pms_api_transaction_type or None, + isReconcilied=(payment.reconciled_statements_count > 0), + downPaymentInvoiceId=payment.reconciled_invoice_ids.filtered( + lambda inv: inv._is_downpayment() + ), + ) + ) + return payments + + @restapi.method( + [ + ( + [ + "//invoices", + ], + "GET", + ) + ], + output_param=Datamodel("pms.invoice.info", is_list=True), + auth="jwt_api_pms", + ) + def get_partner_invoices(self, partner_id): + partnerInvoices = self.env["account.move"].search( + [ + ("partner_id", "=", partner_id), + ("move_type", "in", self.env["account.move"].get_invoice_types()), + ] + ) + invoices = [] + PmsFolioInvoiceInfo = self.env.datamodels["pms.invoice.info"] + PmsInvoiceLineInfo = self.env.datamodels["pms.invoice.line.info"] + if partnerInvoices: + for move in partnerInvoices: + move_lines = [] + for move_line in move.invoice_line_ids: + move_lines.append( + PmsInvoiceLineInfo( + id=move_line.id, + name=move_line.name if move_line.name else None, + quantity=move_line.quantity if move_line.quantity else None, + priceUnit=move_line.price_unit + if move_line.price_unit + else None, + total=move_line.price_total + if move_line.price_total + else None, + discount=move_line.discount if move_line.discount else None, + displayType=move_line.display_type + if move_line.display_type + else None, + saleLineId=move_line.folio_line_ids[0] + if move_line.folio_line_ids + else None, + isDownPayment=move_line.move_id._is_downpayment(), + ) + ) + move_url = ( + move.get_proforma_portal_url() + if move.state == "draft" + else move.get_portal_url() + ) + portal_url = ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url") + + move_url + ) + invoice_date = ( + move.invoice_date.strftime("%d/%m/%Y") + if move.invoice_date + else move.invoice_date_due.strftime("%d/%m/%Y") + if move.invoice_date_due + else None + ) + invoices.append( + PmsFolioInvoiceInfo( + id=move.id if move.id else None, + folioId=move.folio_ids[0] if move.folio_ids else None, + name=move.name if move.name else None, + amount=round(move.amount_total, 2) + if move.amount_total + else None, + date=invoice_date, + state=move.state if move.state else None, + paymentState=move.payment_state if move.payment_state else None, + partnerName=move.partner_id.name + if move.partner_id.name + else None, + partnerId=move.partner_id.id if move.partner_id.id else None, + moveLines=move_lines if move_lines else None, + portalUrl=portal_url, + moveType=move.move_type, + isReversed=move.payment_state == "reversed", + isDownPaymentInvoice=move._is_downpayment(), + isSimplifiedInvoice=move.journal_id.is_simplified_invoice, + ) + ) + + return invoices + + @restapi.method( + [ + ( + [ + "/vat_number//", + ], + "GET", + ) + ], + output_param=Datamodel("pms.partner.info", is_list=False), + auth="jwt_api_pms", + ) + def get_partner_by_vat_number(self, vat_number): + partner = self.env["res.partner"].search( + [ + "|", + ("vat", "ilike", vat_number), + ("aeat_identification", "ilike", vat_number), + ], + limit=1, + ) + PmsPartnerService = self.env.datamodels["pms.partner.info"] + if not partner: + return PmsPartnerService() + return PmsPartnerService( + id=partner.id, + name=partner.name if partner.name else None, + firstname=partner.firstname if partner.firstname else None, + lastname=partner.lastname if partner.lastname else None, + lastname2=partner.lastname2 if partner.lastname2 else None, + email=partner.email if partner.email else None, + mobile=partner.mobile if partner.mobile else None, + phone=partner.phone if partner.phone else None, + documentType=partner.id_numbers[0].category_id.id + if partner.id_numbers + else None, + documentNumber=partner.id_numbers[0].name if partner.id_numbers else None, + documentExpeditionDate=datetime.combine( + partner.id_numbers[0].valid_from, datetime.min.time() + ).isoformat() + if partner.id_numbers + else None, + documentSupportNumber=partner.id_numbers[0].support_number + if partner.id_numbers + else None, + documentCountryId=partner.id_numbers[0].country_id.id + if partner.id_numbers + else None, + gender=partner.gender if partner.gender else None, + birthdate=datetime.combine( + partner.birthdate_date, datetime.min.time() + ).isoformat() + if partner.birthdate_date + else None, + age=partner.age if partner.age else None, + residenceStreet=partner.residence_street + if partner.residence_street + else None, + residenceStreet2=partner.residence_street2 + if partner.residence_street2 + else None, + residenceCity=partner.residence_city if partner.residence_city else None, + residenceZip=partner.residence_zip if partner.residence_zip else None, + nationality=partner.nationality_id.id if partner.nationality_id else None, + residenceStateId=partner.residence_state_id.id + if partner.residence_state_id + else None, + isAgency=partner.is_agency, + isCompany=partner.is_company, + street=partner.street if partner.street else None, + street2=partner.street2 if partner.street2 else None, + zip=partner.zip if partner.zip else None, + city=partner.city if partner.city else None, + stateId=partner.state_id.id if partner.state_id else None, + countryId=partner.country_id.id if partner.country_id else None, + residenceCountryId=partner.residence_country_id.id + if partner.residence_country_id + else None, + vatNumber=partner.vat + if partner.vat + else partner.aeat_identification + if partner.aeat_identification + else None, + vatDocumentType="02" + if partner.vat + else partner.aeat_identification_type + if partner.aeat_identification_type + else None, + comment=partner.comment if partner.comment else None, + language=partner.lang if partner.lang else None, + userId=partner.user_id if partner.user_id else None, + paymentTerms=partner.property_payment_term_id + if partner.property_payment_term_id + else None, + salesReference=partner.ref if partner.ref else None, + pricelistId=partner.property_product_pricelist + if partner.property_product_pricelist + else None, + saleChannelId=partner.sale_channel_id if partner.sale_channel_id else None, + commission=partner.default_commission + if partner.default_commission + else None, + invoicingPolicy=partner.invoicing_policy + if partner.invoicing_policy + else None, + daysAutoInvoice=partner.margin_days_autoinvoice + if partner.margin_days_autoinvoice + else None, + invoicingMonthDay=partner.invoicing_month_day + if partner.invoicing_month_day + else None, + invoiceToAgency=partner.invoice_to_agency + if partner.invoice_to_agency + else None, + tagIds=partner.category_id.ids if partner.category_id else [], + ) + + @restapi.method( + [ + ( + [ + "//", + ], + "GET", + ) + ], + output_param=Datamodel("pms.partner.info", is_list=True), + auth="jwt_api_pms", + ) + def get_checkin_partner_by_partner_doc_number(self, document_type, document_number): + doc_type = self.env["res.partner.id_category"].search( + [("id", "=", document_type)] + ) + # Clean Document number + document_number = re.sub(r"[^a-zA-Z0-9]", "", document_number).upper() + partner = self.env["pms.checkin.partner"]._get_partner_by_document( + document_number, doc_type + ) + partners = [] + if partner: + doc_record = partner.id_numbers.filtered( + lambda doc: doc.category_id.id == doc_type.id + ) + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] + partners.append( + PmsCheckinPartnerInfo( + partnerId=partner.id or None, + name=partner.name or None, + firstname=partner.firstname or None, + lastname=partner.lastname or None, + lastname2=partner.lastname2 or None, + email=partner.email or None, + mobile=partner.mobile or None, + documentType=doc_type.id or None, + documentNumber=doc_record.name or None, + documentExpeditionDate=datetime.combine( + doc_record.valid_from, datetime.min.time() + ).isoformat() + if doc_record.valid_from + else None, + documentSupportNumber=doc_record.support_number or None, + documentCountryId=doc_record.country_id.id or None, + gender=partner.gender or None, + birthdate=datetime.combine( + partner.birthdate_date, datetime.min.time() + ).isoformat() + if partner.birthdate_date + else None, + residenceStreet=partner.residence_street or None, + zip=partner.residence_zip or None, + residenceCity=partner.residence_city or None, + nationality=partner.nationality_id.id or None, + countryId=partner.residence_country_id or None, + countryState=partner.residence_state_id.id or None, + ) + ) + return partners + + @restapi.method( + [ + ( + [ + "/check-doc-number//" + "/", + ], + "GET", + ) + ], + auth="public", + ) + # REVIEW: create a new datamodel and service for documents? + def check_document_number(self, document_number, document_type_id, country_id): + error_mens = False + country = self.env["res.country"].sudo().browse(country_id) + document_type = ( + self.env["res.partner.id_category"].sudo().browse(document_type_id) + ) + id_number = ( + self.env["res.partner.id_number"] + .sudo() + .new( + { + "name": document_number, + "category_id": document_type, + } + ) + ) + try: + document_type.validate_id_number(id_number) + except ValidationError as e: + error_mens = str(e) + if document_type.code == "D": + Partner = self.env["res.partner"] + error = not Partner.simple_vat_check( + country_code=country.code, + vat_number=document_number, + ) + if error: + error_mens = self._construct_check_vat_error_msg( + vat_number=document_number, country_code=country.code + ) + if error_mens: + raise ValidationError(error_mens) + + @restapi.method( + [ + ( + [ + "/p//deactivate", + ], + "PATCH", + ) + ], + auth="jwt_api_pms", + ) + def deactivate_partner(self, partner_id): + self.env["res.partner"].browse(partner_id).active = False + + @restapi.method( + [ + ( + [ + "/p//activate", + ], + "PATCH", + ) + ], + auth="jwt_api_pms", + ) + def activate_partner(self, partner_id): + self.env["res.partner"].browse(partner_id).active = True + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.partner.info", is_list=False), + auth="jwt_api_pms", + ) + def get_partner(self, partner_id): + PmsPartnerInfo = self.env.datamodels["pms.partner.info"] + partner = self.env["res.partner"].browse(partner_id) + if not partner: + return PmsPartnerInfo() + else: + return PmsPartnerInfo( + id=partner.id, + name=partner.name if partner.name else None, + firstname=partner.firstname if partner.firstname else None, + lastname=partner.lastname if partner.lastname else None, + lastname2=partner.lastname2 if partner.lastname2 else None, + email=partner.email if partner.email else None, + mobile=partner.mobile if partner.mobile else None, + phone=partner.phone if partner.phone else None, + gender=partner.gender if partner.gender else None, + birthdate=datetime.combine( + partner.birthdate_date, datetime.min.time() + ).isoformat() + if partner.birthdate_date + else None, + age=partner.age if partner.age else None, + residenceStreet=partner.residence_street + if partner.residence_street + else None, + residenceStreet2=partner.residence_street2 + if partner.residence_street2 + else None, + residenceZip=partner.residence_zip if partner.residence_zip else None, + residenceCity=partner.residence_city + if partner.residence_city + else None, + nationality=partner.nationality_id.id + if partner.nationality_id + else None, + residenceStateId=partner.residence_state_id.id + if partner.residence_state_id + else None, + street=partner.street if partner.street else None, + street2=partner.street2 if partner.street2 else None, + zip=partner.zip if partner.zip else None, + countryId=partner.country_id.id if partner.country_id else None, + stateId=partner.state_id.id if partner.state_id else None, + city=partner.city if partner.city else None, + isAgency=partner.is_agency, + isCompany=partner.is_company, + residenceCountryId=partner.residence_country_id.id + if partner.residence_country_id + else None, + vatNumber=partner.vat + if partner.vat + else partner.aeat_identification + if partner.aeat_identification + else None, + vatDocumentType="02" + if partner.vat + else partner.aeat_identification_type + if partner.aeat_identification_type + else None, + comment=partner.comment if partner.comment else None, + language=partner.lang if partner.lang else None, + userId=partner.user_id if partner.user_id else None, + paymentTerms=partner.property_payment_term_id + if partner.property_payment_term_id + else None, + pricelistId=partner.property_product_pricelist + if partner.property_product_pricelist + else None, + salesReference=partner.ref if partner.ref else None, + saleChannelId=partner.sale_channel_id + if partner.sale_channel_id + else None, + commission=partner.default_commission + if partner.default_commission + else None, + invoicingPolicy=partner.invoicing_policy + if partner.invoicing_policy + else None, + daysAutoInvoice=partner.margin_days_autoinvoice + if partner.margin_days_autoinvoice + else None, + invoicingMonthDay=partner.invoicing_month_day + if partner.invoicing_month_day + else None, + invoiceToAgency=partner.invoice_to_agency + if partner.invoice_to_agency + else None, + ) + + def mapping_partner_values(self, pms_partner_info): + vals = dict() + partner_fields = { + "firstname": pms_partner_info.firstname, + "lastname": pms_partner_info.lastname, + "lastname2": pms_partner_info.lastname2, + "email": pms_partner_info.email, + "mobile": pms_partner_info.mobile, + "phone": pms_partner_info.phone, + "gender": pms_partner_info.gender, + "residence_street": pms_partner_info.residenceStreet, + "residence_street2": pms_partner_info.residenceStreet2, + "nationality_id": pms_partner_info.nationality, + "residence_zip": pms_partner_info.residenceZip, + "residence_city": pms_partner_info.residenceCity, + "residence_state_id": pms_partner_info.residenceStateId, + "residence_country_id": pms_partner_info.residenceCountryId, + "is_agency": pms_partner_info.isAgency, + "is_company": pms_partner_info.isCompany, + "street": pms_partner_info.street, + "street2": pms_partner_info.street2, + "zip": pms_partner_info.zip, + "city": pms_partner_info.city, + "state_id": pms_partner_info.stateId, + "country_id": pms_partner_info.countryId, + "user_id": pms_partner_info.userId, + "lang": pms_partner_info.language, + "comment": pms_partner_info.comment, + "property_payment_term_id": pms_partner_info.paymentTerms, + "property_product_pricelist": pms_partner_info.pricelistId, + "ref": pms_partner_info.salesReference, + "sale_channel_id": pms_partner_info.saleChannelId, + "default_commission": pms_partner_info.commission, + "invoicing_policy": pms_partner_info.invoicingPolicy, + "margin_days_autoinvoice": pms_partner_info.daysAutoInvoice, + "invoicing_month_day": pms_partner_info.invoicingMonthDay, + "invoice_to_agency": pms_partner_info.invoiceToAgency, + } + + if ( + pms_partner_info.isAgency + or pms_partner_info.isCompany + or ( + pms_partner_info.vatDocumentType == "02" + or pms_partner_info.vatDocumentType == "04" + ) + ): + partner_fields.update( + { + "vat": pms_partner_info.vatNumber, + } + ) + else: + partner_fields.update( + { + "aeat_identification_type": pms_partner_info.vatDocumentType, + "aeat_identification": pms_partner_info.vatNumber, + } + ) + + if pms_partner_info.birthdate: + birthdate = datetime.strptime(pms_partner_info.birthdate, "%d/%m/%Y") + birthdate = birthdate.strftime("%Y-%m-%d") + vals.update({"birthdate_date": birthdate}) + for k, v in partner_fields.items(): + if v is not None: + vals.update({k: v}) + return vals + + def _construct_check_vat_error_msg(self, vat_number, country_code): + country_code = country_code.lower() + vat_no = "(##=VAT Number)" + vat_no = _ref_vat.get(country_code) or vat_no + if self.env.context.get("company_id"): + company = self.env["res.company"].browse(self.env.context["company_id"]) + else: + company = self.env.company + if company.vat_check_vies: + return "\n" + _( + "The VAT number [%(vat)s] either failed the VIES VAT " + "validation check or did not respect the expected format %(format)s.", + vat=vat_number, + format=vat_no, + ) + return "\n" + _( + "The VAT number [%(vat)s] does not seem to be valid. " + "\nNote: the expected format is %(format)s", + vat=vat_number, + format=vat_no, + ) diff --git a/pms_api_rest/services/pms_price_service.py b/pms_api_rest/services/pms_price_service.py new file mode 100644 index 0000000000..0383e18e07 --- /dev/null +++ b/pms_api_rest/services/pms_price_service.py @@ -0,0 +1,198 @@ +from datetime import datetime, timedelta + +from odoo import _, fields +from odoo.exceptions import MissingError, ValidationError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsPriceService(Component): + _inherit = "base.rest.service" + _name = "pms.price.service" + _usage = "prices" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.price.search.param"), + output_param=Datamodel("pms.price.info", is_list=True), + auth="jwt_api_pms", + ) + def get_prices(self, prices_search_param): + product = room_type = board_service_room_type = False + + if prices_search_param.roomTypeId: + room_type = self.env["pms.room.type"].search( + [("id", "=", prices_search_param.roomTypeId)] + ) + elif prices_search_param.productId: + product = self.env["product.product"].search( + [("id", "=", prices_search_param.productId)] + ) + elif ( + prices_search_param.boardServiceLineId + and not prices_search_param.boardServiceId + ): + raise ValidationError( + _("Mandatory board service id is missing for board service line") + ) + elif prices_search_param.boardServiceId: + if prices_search_param.isAdults and prices_search_param.isChildren: + raise ValidationError( + _( + "It is not allowed to filter by adults and children simultaneously" + ) + ) + if ( + not prices_search_param.isAdults + and not prices_search_param.isChildren + and not prices_search_param.boardServiceLineId + ): + raise ValidationError( + _( + """ + It is necessary to indicate whether + the board service is for adults or children. + """ + ) + ) + board_service_room_type = self.env["pms.board.service.room.type"].search( + [("id", "=", prices_search_param.boardServiceId)] + ) + else: + raise MissingError(_("Wrong input param")) + + PmsPriceInfo = self.env.datamodels["pms.price.info"] + result_prices = [] + date_from = fields.Date.from_string(prices_search_param.dateFrom) + date_to = fields.Date.from_string(prices_search_param.dateTo) + dates = [ + date_from + timedelta(days=x) + for x in range(0, (date_to - date_from).days + 1) + ] + for price_date in dates: + if board_service_room_type: + result_prices.append( + PmsPriceInfo( + date=datetime.combine( + price_date, datetime.min.time() + ).isoformat(), + price=round( + self._get_board_service_price( + board_service=board_service_room_type, + board_type="adults" + if prices_search_param.isAdults + else "children", + pms_property_id=prices_search_param.pmsPropertyId, + pricelist_id=prices_search_param.pricelistId, + partner_id=prices_search_param.partnerId, + product_qty=prices_search_param.productQty, + date_consumption=price_date, + board_service_line_id=prices_search_param.boardServiceLineId + or False, + ), + 2, + ), + ) + ) + else: + result_prices.append( + PmsPriceInfo( + date=datetime.combine( + price_date, datetime.min.time() + ).isoformat(), + price=round( + self._get_product_price( + product=product if product else room_type.product_id, + pms_property_id=prices_search_param.pmsPropertyId, + pricelist_id=prices_search_param.pricelistId, + partner_id=prices_search_param.partnerId, + product_qty=prices_search_param.productQty, + date_consumption=price_date, + ), + 2, + ), + ) + ) + return result_prices + + def _get_product_price( + self, + product, + pms_property_id, + pricelist_id=False, + partner_id=False, + product_qty=False, + date_consumption=False, + board_service_id=False, + board_service_line_id=False, + ): + pms_property = self.env["pms.property"].browse(pms_property_id) + product_context = dict( + self.env.context, + date=datetime.today().date(), + pricelist=pricelist_id, + uom=product.uom_id.id, + fiscal_position=False, + property=pms_property_id, + ) + if date_consumption: + product_context["consumption_date"] = date_consumption + if board_service_line_id: + product_context["board_service_line_id"] = board_service_line_id + product = product.with_context(product_context) + return self.env["account.tax"]._fix_tax_included_price_company( + self.env["product.product"]._pms_get_display_price( + pricelist_id=pricelist_id, + product=product, + company_id=pms_property.company_id.id, + product_qty=product_qty or 1, + partner_id=partner_id, + ), + product.taxes_id, + product.taxes_id, # Not exist service line, we repeat product taxes + pms_property.company_id, + ) + + def _get_board_service_price( + self, + board_service, + board_type, + pms_property_id, + pricelist_id=False, + partner_id=False, + product_qty=False, + date_consumption=False, + board_service_line_id=False, + ): + price = 0 + if board_service_line_id: + lines = self.env["pms.board.service.room.type.line"].browse( + board_service_line_id + ) + else: + lines = board_service.board_service_line_ids.filtered( + lambda l: l.adults if board_type == "adults" else l.children + ) + print(lines) + for line in lines: + price += self._get_product_price( + product=line.product_id, + pms_property_id=pms_property_id, + pricelist_id=pricelist_id, + partner_id=partner_id, + product_qty=product_qty or 1, + date_consumption=date_consumption, + board_service_id=board_service.id, + board_service_line_id=line.id, + ) + return price diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py new file mode 100644 index 0000000000..cf18150976 --- /dev/null +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -0,0 +1,263 @@ +from datetime import datetime, timedelta + +from odoo import _ +from odoo.exceptions import MissingError, ValidationError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsPricelistService(Component): + _inherit = "base.rest.service" + _name = "pms.pricelist.service" + _usage = "pricelists" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.pricelist.search", is_list=False), + output_param=Datamodel("pms.pricelist.info", is_list=True), + auth="jwt_api_pms", + ) + def get_pricelists(self, pms_search_param, **args): + domain = [ + ("is_pms_available", "=", True), + ] + if pms_search_param.daily and pms_search_param.daily is True: + domain.append(("pricelist_type", "=", "daily")) + + pricelists = self.env["product.pricelist"].search(domain) + if pms_search_param.pmsPropertyIds and pms_search_param.pmsPropertyId: + raise ValidationError( + _( + """ + Simultaneous search by list of properties and by specific property: + make sure to use only one of the two search parameters + """ + ) + ) + if pms_search_param.pmsPropertyIds: + pricelists = pricelists.filtered( + lambda p: not p.pms_property_ids + or all( + item in p.pms_property_ids.ids + for item in pms_search_param.pmsPropertyIds + ) + ) + if pms_search_param.pmsPropertyId: + pricelists = pricelists.filtered( + lambda p: not p.pms_property_ids + or pms_search_param.pmsPropertyId in p.pms_property_ids.ids + ) + if pms_search_param.saleChannelId: + pricelists = pricelists.filtered( + lambda p: not p.pms_sale_channel_ids + or pms_search_param.saleChannelId in p.pms_sale_channel_ids.ids + ) + PmsPricelistInfo = self.env.datamodels["pms.pricelist.info"] + result_pricelists = [] + for pricelist in pricelists: + result_pricelists.append( + PmsPricelistInfo( + id=pricelist.id, + name=pricelist.name, + cancelationRuleId=pricelist.cancelation_rule_id.id + if pricelist.cancelation_rule_id + else None, + defaultAvailabilityPlanId=pricelist.availability_plan_id.id + if pricelist.availability_plan_id + else None, + pmsPropertyIds=pricelist.pms_property_ids.ids, + saleChannelIds=pricelist.pms_sale_channel_ids.ids, + ) + ) + return result_pricelists + + @restapi.method( + [ + ( + [ + "/restricted/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.pricelist.info", is_list=False), + auth="jwt_api_pms", + ) + def get_pricelist(self, pricelist_id, **args): + pricelist = self.env["product.pricelist"].sudo().browse(pricelist_id) + if pricelist.exists(): + PmsPricelistInfo = self.env.datamodels["pms.pricelist.info"] + return PmsPricelistInfo( + id=pricelist.id, + name=pricelist.name, + cancelationRuleId=pricelist.cancelation_rule_id.id + if pricelist.cancelation_rule_id + else None, + defaultAvailabilityPlanId=pricelist.availability_plan_id.id + if pricelist.availability_plan_id + else None, + pmsPropertyIds=pricelist.pms_property_ids.ids, + saleChannelIds=pricelist.pms_sale_channel_ids.ids, + ) + else: + raise MissingError(_("Pricelist not found")) + + @restapi.method( + [ + ( + [ + "//pricelist-items", + ], + "GET", + ) + ], + input_param=Datamodel("pms.pricelist.item.search.param", is_list=False), + output_param=Datamodel("pms.pricelist.item.info", is_list=True), + auth="jwt_api_pms", + ) + def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): + pms_property = self.env["pms.property"].browse( + pricelist_item_search_param.pmsPropertyId + ) + date_from = datetime.strptime( + pricelist_item_search_param.dateFrom, "%Y-%m-%d" + ).date() + date_to = datetime.strptime( + pricelist_item_search_param.dateTo, "%Y-%m-%d" + ).date() + count_nights = (date_to - date_from).days + 1 + target_dates = [date_from + timedelta(days=x) for x in range(count_nights)] + record_pricelist = self.env["product.pricelist"].sudo.browse(pricelist_id) + if not record_pricelist.exists(): + raise MissingError + rooms = ( + self.env["pms.room"] + .with_context(active_test=True) + .search( + [("pms_property_id", "=", pricelist_item_search_param.pmsPropertyId)] + ) + ) + room_types = rooms.mapped("room_type_id") + result = [] + PmsPricelistItemInfo = self.env.datamodels["pms.pricelist.item.info"] + for date in target_dates: + products = [(product, 1, False) for product in room_types.product_id] + date_prices = record_pricelist.with_context( + quantity=1, + consumption_date=date, + property=pms_property.id, + )._compute_price_rule(products, datetime.today()) + for product_id, v in date_prices.items(): + room_type_id = ( + self.env["product.product"].browse(product_id).room_type_id.id + ) + if not v[1]: + continue + pricelist_info = PmsPricelistItemInfo( + roomTypeId=room_type_id, + date=str(datetime.combine(date, datetime.min.time()).isoformat()), + pricelistItemId=v[1], + price=v[0], + ) + result.append(pricelist_info) + return result + + def _create_or_update_pricelist_items(self, pms_pricelist_item_info): + for pms_pricelist_item in pms_pricelist_item_info.pricelistItems: + date = datetime.strptime(pms_pricelist_item.date, "%Y-%m-%d").date() + product_id = ( + self.env["pms.room.type"] + .browse(pms_pricelist_item.roomTypeId) + .product_id + ) + product_pricelist_item = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", pms_pricelist_item.pricelistId), + ("product_id", "=", product_id.id), + ("pms_property_ids", "in", pms_pricelist_item.pmsPropertyId), + ("date_start_consumption", "=", date), + ("date_end_consumption", "=", date), + ] + ) + if product_pricelist_item: + product_pricelist_item.write( + { + "fixed_price": pms_pricelist_item.price, + } + ) + else: + self.env["product.pricelist.item"].create( + { + "applied_on": "0_product_variant", + "product_id": product_id.id, + "pms_property_ids": [pms_pricelist_item.pmsPropertyId], + "date_start_consumption": date, + "date_end_consumption": date, + "compute_price": "fixed", + "fixed_price": pms_pricelist_item.price, + "pricelist_id": pms_pricelist_item.pricelistId, + } + ) + + @restapi.method( + [ + ( + [ + "//pricelist-items", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.pricelist.items.info", is_list=False), + auth="jwt_api_pms", + ) + def create_pricelist_item_old(self, pricelist_id, pms_pricelist_item_info): + self._create_or_update_pricelist_items(pms_pricelist_item_info) + + @restapi.method( + [ + ( + [ + "/p//pricelist-items", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.pricelist.items.info", is_list=False), + auth="jwt_api_pms", + ) + def create_pricelist_item_fix_patch(self, pricelist_id, pms_pricelist_item_info): + pricelist_ids = list( + {item.pricelistId for item in pms_pricelist_item_info.pricelistItems} + ) + if len(pricelist_ids) > 1 or pricelist_ids[0] != pricelist_id: + raise ValidationError( + _("You cannot create pricelist items for different pricelists at once.") + ) + else: + self._create_or_update_pricelist_items(pms_pricelist_item_info) + + @restapi.method( + [ + ( + [ + "/batch-changes", + ], + "POST", + ) + ], + input_param=Datamodel("pms.pricelist.items.info", is_list=False), + auth="jwt_api_pms", + ) + def update_availability_plan_rules(self, pms_avail_plan_rules_info): + self._create_or_update_pricelist_items(pms_avail_plan_rules_info) diff --git a/pms_api_rest/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py new file mode 100644 index 0000000000..052cbfe25d --- /dev/null +++ b/pms_api_rest/services/pms_product_service.py @@ -0,0 +1,109 @@ +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsProductService(Component): + _inherit = "base.rest.service" + _name = "pms.product.service" + _usage = "products" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.product.search.param"), + output_param=Datamodel("pms.product.info", is_list=True), + auth="jwt_api_pms", + ) + def get_products(self, product_search_param): + domain = [("sale_ok", "=", True), ("is_pms_available", "=", True)] + if product_search_param.name: + domain.append(("name", "ilike", product_search_param.name)) + if product_search_param.pmsPropertyId: + domain.extend( + [ + "|", + ( + "pms_property_ids", + "in", + product_search_param.pmsPropertyId, + ), + ("pms_property_ids", "=", False), + ] + ) + result_products = [] + PmsProductInfo = self.env.datamodels["pms.product.info"] + for product in self.env["product.product"].search( + domain, + ): + result_products.append( + PmsProductInfo( + id=product.id, + name=product.name, + perDay=product.per_day, + perPerson=product.per_person, + consumedOn=product.consumed_on, + ) + ) + return result_products + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.product.search.param"), + output_param=Datamodel("pms.product.info", is_list=False), + auth="jwt_api_pms", + ) + def get_product(self, product_id, product_search_param): + product = self.env["product.product"].browse(product_id) + if product and product.sale_ok: + PmsProductInfo = self.env.datamodels["pms.product.info"] + return PmsProductInfo( + id=product.id, + name=product.name, + perDay=product.per_day, + perPerson=product.per_person, + ) + else: + raise MissingError(_("Product not found")) + + @restapi.method( + [ + ( + [ + "/restricted/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.product.info", is_list=False), + auth="jwt_api_pms", + ) + def get_product(self, product_id, product_search_param): + product = self.env["product.product"].sudo().browse(product_id) + if product.exists() and product.sale_ok: + PmsProductInfo = self.env.datamodels["pms.product.info"] + return PmsProductInfo( + id=product.id, + name=product.name, + perDay=product.per_day, + perPerson=product.per_person, + ) + else: + raise MissingError(_("Product not found")) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py new file mode 100644 index 0000000000..c174d49536 --- /dev/null +++ b/pms_api_rest/services/pms_property_service.py @@ -0,0 +1,273 @@ +import base64 +import re + +from odoo import fields + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +from ..pms_api_rest_utils import url_image_pms_api_rest + + +class PmsPropertyService(Component): + _inherit = "base.rest.service" + _name = "pms.property.service" + _usage = "properties" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.property.info", is_list=True), + auth="jwt_api_pms", + ) + def get_properties(self): + domain = [("user_ids", "in", [self.env.user.id])] + result_properties = [] + PmsPropertyInfo = self.env.datamodels["pms.property.info"] + for prop in self.env["pms.property"].search( + domain, + ): + state_name = False + ine_category = False + privacy_policy = False + tokens_to_replace = re.compile("<.*?>") + if prop.company_id.privacy_policy: + privacy_policy = re.sub( + tokens_to_replace, "", prop.company_id.privacy_policy + ) + if prop.state_id: + state_name = ( + self.env["res.country.state"] + .search([("id", "=", prop.state_id.id)]) + .name + ) + if prop.ine_category_id: + ine_category = ( + prop.ine_category_id.category + + " (" + + prop.ine_category_id.type + + ")" + ) + result_properties.append( + PmsPropertyInfo( + id=prop.id, + name=prop.name, + defaultPricelistId=prop.default_pricelist_id.id, + colorOptionConfig=prop.color_option_config, + preReservationColor=prop.pre_reservation_color, + confirmedReservationColor=prop.confirmed_reservation_color, + paidReservationColor=prop.paid_reservation_color, + onBoardReservationColor=prop.on_board_reservation_color, + paidCheckinReservationColor=prop.paid_checkin_reservation_color, + outReservationColor=prop.out_reservation_color, + staffReservationColor=prop.staff_reservation_color, + toAssignReservationColor=prop.to_assign_reservation_color, + pendingPaymentReservationColor=prop.pending_payment_reservation_color, + overPaymentColor=prop.overpayment_reservation_color, + simpleOutColor=prop.simple_out_color, + simpleInColor=prop.simple_in_color, + simpleFutureColor=prop.simple_future_color, + language=prop.lang, + isOCRAvailable=True if prop.ocr_checkin_supplier else False, + hotelImageUrl=url_image_pms_api_rest( + "pms.property", prop.id, "hotel_image_pms_api_rest" + ), + street=prop.street if prop.street else None, + street2=prop.street2 if prop.street2 else None, + zip=prop.zip if prop.zip else None, + city=prop.city if prop.city else None, + stateName=state_name if state_name else None, + ineCategory=ine_category if ine_category else None, + cardexWarning=prop.cardex_warning if prop.cardex_warning else None, + companyPrivacyPolicy=privacy_policy + if prop.company_id.privacy_policy + else None, + canDownloadIneReport=True + if (prop.ine_tourism_number and prop.ine_category_id) + else False, + companyName=prop.company_id.name, + maxAmountSimplifiedInvoice=prop.max_amount_simplified_invoice + if prop.max_amount_simplified_invoice + else None, + ) + ) + return result_properties + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.property.info"), + auth="jwt_api_pms", + ) + def get_property(self, property_id): + pms_property = self.env["pms.property"].search([("id", "=", property_id)]) + res = [] + PmsPropertyInfo = self.env.datamodels["pms.property.info"] + if not pms_property: + pass + else: + state_name = False + ine_category = False + if pms_property.state_id: + state_name = ( + self.env["res.country.state"] + .search([("id", "=", pms_property.state_id.id)]) + .name + ) + if pms_property.ine_category_id: + ine_category = ( + pms_property.ine_category_id.category + + " (" + + pms_property.ine_category_id.type + + ")" + ) + tokens_to_replace = re.compile("<.*?>") + privacy_policy = re.sub( + tokens_to_replace, "", pms_property.company_id.privacy_policy + ) + res = PmsPropertyInfo( + id=pms_property.id, + name=pms_property.name, + company=pms_property.company_id.name, + defaultPricelistId=pms_property.default_pricelist_id.id, + colorOptionConfig=pms_property.color_option_config, + preReservationColor=pms_property.pre_reservation_color, + confirmedReservationColor=pms_property.confirmed_reservation_color, + paidReservationColor=pms_property.paid_reservation_color, + onBoardReservationColor=pms_property.on_board_reservation_color, + paidCheckinReservationColor=pms_property.paid_checkin_reservation_color, + outReservationColor=pms_property.out_reservation_color, + staffReservationColor=pms_property.staff_reservation_color, + toAssignReservationColor=pms_property.to_assign_reservation_color, + pendingPaymentReservationColor=pms_property.pending_payment_reservation_color, + language=pms_property.lang, + street=pms_property.street if pms_property.street else None, + street2=pms_property.street2 if pms_property.street2 else None, + zip=pms_property.zip if pms_property.zip else None, + city=pms_property.city if pms_property.city else None, + stateName=state_name if state_name else None, + ineCategory=ine_category if ine_category else None, + cardexWarning=pms_property.cardex_warning + if pms_property.cardex_warning + else None, + companyPrivacyPolicy=privacy_policy + if pms_property.company_id.privacy_policy + else None, + isUsedOCR=True if pms_property.ocr_checkin_supplier else False, + canDownloadIneReport=True + if (pms_property.ine_tourism_number and pms_property.ine_category_id) + else False, + maxAmountSimplifiedInvoice=pms_property.max_amount_simplified_invoice + if pms_property.max_amount_simplified_invoice + else None, + ) + + return res + + @restapi.method( + [ + ( + [ + "//users", + ], + "GET", + ) + ], + output_param=Datamodel("res.users.info", is_list=True), + auth="jwt_api_pms", + ) + def get_users(self, pms_property_id): + result_users = [] + ResUsersInfo = self.env.datamodels["res.users.info"] + users = self.env["res.users"].search( + [("pms_property_ids", "in", pms_property_id)] + ) + for user in users: + result_users.append( + ResUsersInfo( + id=user.id, + name=user.name, + # TODO: Disabled by performance issues + # userImageBase64=user.partner_id.image_1024 or None, + userImageBase64=None, + ) + ) + return result_users + + @restapi.method( + [ + ( + [ + "/ine-report", + ], + "GET", + ) + ], + input_param=Datamodel("pms.report.search.param", is_list=False), + output_param=Datamodel("pms.report", is_list=False), + auth="jwt_api_pms", + ) + def ine_report(self, pms_report_search_param): + pms_property_id = pms_report_search_param.pmsPropertyId + date_from = fields.Date.from_string(pms_report_search_param.dateFrom) + date_to = fields.Date.from_string(pms_report_search_param.dateTo) + report_wizard = self.env["pms.ine.wizard"].create( + { + "start_date": date_from, + "end_date": date_to, + "pms_property_id": pms_property_id, + } + ) + report_wizard.ine_generate_xml() + # file_name is INE__.xml + file_name = ( + "INE_" + date_from.strftime("%m") + "_" + date_from.strftime("%Y") + ".xml" + ) + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(fileName=file_name, binary=report_wizard.txt_binary) + + @restapi.method( + [ + ( + [ + "/traveller-report", + ], + "GET", + ) + ], + input_param=Datamodel("pms.report.search.param", is_list=False), + output_param=Datamodel("pms.report", is_list=False), + auth="jwt_api_pms", + ) + def transactions_report(self, pms_report_search_param): + pms_property_id = pms_report_search_param.pmsPropertyId + pms_property = self.env["pms.property"].search([("id", "=", pms_property_id)]) + date_from = fields.Date.from_string(pms_report_search_param.dateFrom) + report_wizard = self.env["traveller.report.wizard"].create( + { + "date_target": date_from, + "pms_property_id": pms_property_id, + } + ) + content = report_wizard.generate_checkin_list( + pms_property_id=pms_property_id, + date_target=date_from, + ) + file_name = pms_property.institution_property_id + ".999" + base64EncodedStr = base64.b64encode(str.encode(content)) + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(fileName=file_name, binary=base64EncodedStr) diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py new file mode 100644 index 0000000000..dcb137627d --- /dev/null +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -0,0 +1,192 @@ +from datetime import datetime + +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsReservationLineService(Component): + _inherit = "base.rest.service" + _name = "pms.reservation.line.service" + _usage = "reservation-lines" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.reservation.line.info", is_list=False), + auth="jwt_api_pms", + ) + def get_reservation_line(self, reservation_line_id): + reservation_line = self.env["pms.reservation.line"].search( + [("id", "=", reservation_line_id)] + ) + if reservation_line: + PmsReservationLineInfo = self.env.datamodels["pms.reservation.line.info"] + return PmsReservationLineInfo( + id=reservation_line.id, + date=datetime.combine( + reservation_line.date, datetime.min.time() + ).isoformat(), + price=round(reservation_line.price, 2), + discount=round(reservation_line.discount, 2), + cancelDiscount=round(reservation_line.cancel_discount, 2), + roomId=reservation_line.room_id.id, + reservationId=reservation_line.reservation_id.id, + pmsPropertyId=reservation_line.pms_property_id.id, + isReselling=reservation_line.is_reselling, + ) + else: + raise MissingError(_("Reservation Line not found")) + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.reservation.line.search.param", is_list=False), + output_param=Datamodel("pms.reservation.line.info", is_list=True), + auth="jwt_api_pms", + ) + def get_reservation_lines(self, pms_reservation_lines_search_param): + result = [] + if ( + pms_reservation_lines_search_param.dateFrom + and pms_reservation_lines_search_param.dateTo + and pms_reservation_lines_search_param.pmsPropertyId + ): + date_from = datetime.strptime( + pms_reservation_lines_search_param.dateFrom, "%Y-%m-%d" + ).date() + date_to = datetime.strptime( + pms_reservation_lines_search_param.dateTo, "%Y-%m-%d" + ).date() + + domain = [ + ("date", ">=", date_from), + ("date", "<", date_to), + ( + "pms_property_id", + "=", + pms_reservation_lines_search_param.pmsPropertyId, + ), + ] + PmsReservationLineInfo = self.env.datamodels["pms.reservation.line.info"] + for reservation_line in self.env["pms.reservation.line"].search(domain): + result.append( + PmsReservationLineInfo( + id=reservation_line.id, + date=datetime.combine( + reservation_line.date, datetime.min.time() + ).isoformat(), + price=round(reservation_line.price, 2), + discount=round(reservation_line.discount, 2), + cancelDiscount=round(reservation_line.cancel_discount, 2), + roomId=reservation_line.room_id.id, + reservationId=reservation_line.reservation_id.id, + pmsPropertyId=reservation_line.pms_property_id.id, + isReselling=reservation_line.is_reselling, + reservationType=reservation_line.reservation_id.reservation_type, + state=reservation_line.state, + isSplitted=reservation_line.reservation_id.splitted, + ) + ) + return result + + # @restapi.method( + # [ + # ( + # [ + # "/", + # ], + # "GET", + # ) + # ], + # input_param=Datamodel("pms.reservation.line.search.param"), + # output_param=Datamodel("pms.reservation.line.info", is_list=True), + # auth="jwt_api_pms", + # ) + # def get_reservation_lines(self, reservation_lines_search_param): + # domain = [] + # if reservation_lines_search_param.date: + # domain.append(("date", "=", reservation_lines_search_param.date)) + # if reservation_lines_search_param.reservationId: + # domain.append( + # ("reservation_id", "=", reservation_lines_search_param.reservationId) + # ) + # if reservation_lines_search_param.pmsPropertyId: + # domain.extend( + # [ + # ( + # "pms_property_id", + # "=", + # reservation_lines_search_param.pmsPropertyId, + # ), + # ] + # ) + + # result_lines = [] + # PmsReservationLineInfo = self.env.datamodels["pms.reservation.line.info"] + # for reservation_line in self.env["pms.reservation.line"].search( + # domain, + # ): + # result_lines.append( + # PmsReservationLineInfo( + # id=reservation_line.id, + # date=datetime.combine( + # reservation_line.date, datetime.min.time() + # ).isoformat(), + # price=round(reservation_line.price, 2), + # discount=round(reservation_line.discount, 2), + # cancelDiscount=round(reservation_line.cancel_discount, 2), + # roomId=reservation_line.room_id.id, + # reservationId=reservation_line.reservation_id.id, + # pmsPropertyId=reservation_line.pms_property_id.id, + # ) + # ) + # return result_lines + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.reservation.line.info"), + auth="jwt_api_pms", + ) + def update_reservation_line(self, reservation_line_id, reservation_line_info): + reservation_line = self.env["pms.reservation.line"].search( + [("id", "=", reservation_line_id)] + ) + vals = dict() + if reservation_line: + if reservation_line_info.price is not None: + vals["price"] = reservation_line_info.price + if reservation_line_info.discount is not None: + vals["discount"] = reservation_line_info.discount + if reservation_line_info.cancelDiscount is not None: + vals["cancel_discount"] = reservation_line_info.cancelDiscount + if reservation_line_info.roomId: + vals["room_id"] = reservation_line_info.roomId + if reservation_line_info.isReselling is not None: + vals["is_reselling"] = reservation_line_info.isReselling + reservation_line.write(vals) + else: + raise MissingError(_("Reservation Line not found")) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py new file mode 100644 index 0000000000..068f43c414 --- /dev/null +++ b/pms_api_rest/services/pms_reservation_service.py @@ -0,0 +1,1919 @@ +import base64 +import os +import re +import tempfile +from datetime import datetime, timedelta + +from odoo import _, fields +from odoo.exceptions import AccessError, 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.addons.pms_api_rest.pms_api_rest_utils import url_image_pms_api_rest +from odoo.addons.portal.controllers.portal import CustomerPortal + + +def is_adult(birthdate): + if not birthdate: + return False + today = datetime.now() + age = ( + today.year + - birthdate.year + - ((today.month, today.day) < (birthdate.month, birthdate.day)) + ) + return age >= 18 + + +def find_opposite_relationship(relationship_code): + inverse_relationships = { + "PM": "HJ", # Padre o Madre -> Hijo + "TU": "OT", # Tutor -> Otro (sin inverso claro) + "TI": "SB", # Tío -> Sobrino + "HR": "HR", # Hermano -> Hermano + "AB": "NI", # Abuelo -> Nieto + "BA": "BN", # Bisabuelo -> Bisnieto + "CD": "CD", # Cuñado -> Cuñado + "CY": "CY", # Cónyuge -> Cónyuge + "SB": "TI", # Sobrino -> Tío + "SG": "YN", # Suegro -> Yerno o Nuera + "YN": "SG", # Yerno o Nuera -> Suegro + "OT": "OT", # Otro -> Tutor (arbitrario) + } + # Buscar la relación inversa + related_code = inverse_relationships.get( + relationship_code, "OT" + ) # Devuelve 'OT' si el código no es válido + return related_code + + +def remove_html_tags(text): + pattern = re.compile(r"<.*?>") + text_clean = re.sub(pattern, "", text) + return text_clean + + +class PmsReservationService(Component): + _inherit = "base.rest.service" + _name = "pms.reservation.service" + _usage = "reservations" + _collection = "pms.services" + + # ------------------------------------------------------------------------------------ + # HEAD RESERVATION-------------------------------------------------------------------- + # ------------------------------------------------------------------------------------ + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.search.param", is_list=False), + output_param=Datamodel("pms.reservation.info", is_list=False), + auth="jwt_api_pms", + ) + def get_reservation(self, reservation_id, pms_search_param): + domain = list() + domain.append(("id", "=", reservation_id)) + if pms_search_param.pmsPropertyId: + domain.append(("pms_property_id", "=", pms_search_param.pmsPropertyId)) + reservation = self.env["pms.reservation"].search(domain) + res = [] + PmsReservationInfo = self.env.datamodels["pms.reservation.info"] + if not reservation: + pass + else: + # messages = [] + # import re + + # text = re.compile("<.*?>") + # for message in reservation.message_ids.sorted(key=lambda x: x.date): + # messages.append( + # { + # "author": message.author_id.name, + # "date": str(message.date), + # # print(self.env["ir.fields.converter"].text_from_html(message.body)) + # "body": re.sub(text, "", message.body), + # } + # ) + res = PmsReservationInfo( + id=reservation.id, + name=reservation.name, + folioId=reservation.folio_id.id, + folioSequence=reservation.folio_sequence, + partnerId=reservation.partner_id.id if reservation.partner_id else None, + partnerName=reservation.partner_name or None, + boardServiceId=reservation.board_service_room_id.id + if reservation.board_service_room_id + else None, + saleChannelId=reservation.sale_channel_origin_id.id + if reservation.sale_channel_origin_id + else None, + agencyId=reservation.agency_id.id if reservation.agency_id else None, + userId=reservation.user_id.id if reservation.user_id else None, + checkin=datetime.combine( + reservation.checkin, datetime.min.time() + ).isoformat(), + checkout=datetime.combine( + reservation.checkout, datetime.min.time() + ).isoformat(), + arrivalHour=reservation.arrival_hour, + departureHour=reservation.departure_hour, + roomTypeId=reservation.room_type_id.id + if reservation.room_type_id + else None, + preferredRoomId=reservation.preferred_room_id.id + if reservation.preferred_room_id + else None, + pricelistId=reservation.pricelist_id.id + if reservation.pricelist_id + else None, + adults=reservation.adults if reservation.adults else None, + overbooking=reservation.overbooking, + externalReference=reservation.external_reference + if reservation.external_reference + else None, + stateCode=reservation.state, + stateDescription=dict( + reservation.fields_get(["state"])["state"]["selection"] + )[reservation.state], + children=reservation.children if reservation.children else 0, + readyForCheckin=reservation.ready_for_checkin, + checkinPartnerCount=reservation.checkin_partner_count, + allowedCheckout=reservation.allowed_checkout, + isSplitted=reservation.splitted, + pendingCheckinData=reservation.pending_checkin_data, + createDate=reservation.create_date.isoformat(), + createdBy=reservation.create_uid.name, + segmentationId=reservation.segmentation_ids[0].id + if reservation.segmentation_ids + else None, + toAssign=reservation.to_assign, + reservationType=reservation.reservation_type, + priceTotal=round(reservation.price_room_services_set, 2), + priceTax=round(reservation.price_tax, 2), + discount=round(reservation.discount, 2), + servicesDiscount=round(reservation.services_discount, 2), + commissionAmount=round(reservation.commission_amount, 2) + if reservation.commission_amount + else None, + commissionPercent=round(reservation.commission_percent, 2) + if reservation.commission_percent + else None, + priceOnlyServices=round(reservation.price_services, 2), + priceOnlyRoom=round(reservation.price_total, 2), + partnerRequests=reservation.partner_requests + if reservation.partner_requests + else None, + nights=reservation.nights, + numServices=len(reservation.service_ids) + if reservation.service_ids + else 0, + isReselling=any( + line.is_reselling for line in reservation.reservation_line_ids + ), + isBlocked=reservation.blocked, + ) + return res + + def _create_vals_from_params( + self, reservation_vals, reservation_data, reservation_id + ): + if reservation_data.preferredRoomId: + reservation_vals.update( + {"preferred_room_id": reservation_data.preferredRoomId} + ) + if reservation_data.boardServiceId is not None: + reservation_vals.update( + {"board_service_room_id": reservation_data.boardServiceId or False} + ) + if reservation_data.pricelistId: + reservation_vals.update({"pricelist_id": reservation_data.pricelistId}) + if reservation_data.adults: + reservation_vals.update({"adults": reservation_data.adults}) + if reservation_data.children is not None: + reservation_vals.update({"children": reservation_data.children}) + if reservation_data.segmentationId is not None: + if reservation_data.segmentationId != 0: + reservation_vals.update( + {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} + ) + else: + reservation_vals.update({"segmentation_ids": [(5, 0, 0)]}) + if reservation_data.checkin: + reservation_vals.update({"checkin": reservation_data.checkin}) + if reservation_data.checkout: + reservation_vals.update({"checkout": reservation_data.checkout}) + if reservation_data.partnerName: + reservation_vals.update({"partner_name": reservation_data.partnerName}) + if reservation_data.partnerEmail: + reservation_vals.update({"email": reservation_data.partnerEmail}) + if reservation_data.partnerPhone: + reservation_vals.update({"mobile": reservation_data.partnerPhone}) + if reservation_data.partnerId: + if reservation_data.partnerId != 0: + reservation_vals.update({"partner_id": reservation_data.partnerId}) + else: + reservation_vals.update({"partner_id": False}) + + return reservation_vals + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.reservation.info", is_list=False), + auth="jwt_api_pms", + ) + # TODO: route changed because bug route CORS patch + def update_reservation(self, reservation_id, reservation_data): + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + reservation_vals = {} + if reservation_data.reservationLines: + reservation_lines_vals = [] + date_list = [] + for line_data in sorted( + reservation_data.reservationLines, + key=lambda x: datetime.strptime(x.date, "%Y-%m-%d"), + ): + date_line = datetime.strptime(line_data.date, "%Y-%m-%d").date() + date_list.append(date_line) + # 1- update values in existing lines + reservation_line = self.env["pms.reservation.line"].search( + [("reservation_id", "=", reservation_id), ("date", "=", date_line)] + ) + if reservation_line: + line_vals = self._get_reservation_lines_mapped( + line_data, reservation_line + ) + if line_vals: + reservation_lines_vals.append( + (1, reservation_line.id, line_vals) + ) + # 2- create new lines + else: + line_vals = self._get_reservation_lines_mapped(line_data) + line_vals["date"] = line_data.date + reservation_lines_vals.append((0, False, line_vals)) + # 3- delete old lines: + for line in reservation.reservation_line_ids.filtered( + lambda l: l.date not in date_list + ): + reservation_lines_vals.append((2, line.id)) + if reservation_lines_vals: + reservation_vals.update( + { + "reservation_line_ids": reservation_lines_vals, + } + ) + self._update_reservation_state(reservation, reservation_data) + + reservation_vals = self._create_vals_from_params( + reservation_vals, + reservation_data, + reservation_id, + ) + + service_cmds = [] + if ( + reservation_data.boardServiceId is not None + or reservation_data.boardServices is not None + ): + for service in reservation.service_ids.filtered( + lambda x: x.is_board_service + ): + service_cmds.append((2, service.id)) + + if reservation_data.boardServices is not None: + for bs in reservation_data.boardServices: + service_line_cmds = [] + for line in bs.serviceLines: + service_line_cmds.append( + ( + 0, + False, + { + "price_unit": line.priceUnit, + "date": line.date, + "discount": line.discount, + "day_qty": line.quantity, + "auto_qty": True, + }, + ) + ) + service_cmds.append( + ( + 0, + False, + { + "product_id": bs.productId, + "is_board_service": True, + "reservation_id": reservation_id, + "service_line_ids": service_line_cmds, + }, + ) + ) + if service_cmds: + reservation_vals.update({"service_ids": service_cmds}) + + if reservation_vals and reservation_data.boardServices: + reservation.with_context(skip_compute_board_service_ids=True).write( + reservation_vals + ) + elif reservation_vals: + reservation.write(reservation_vals) + + def _update_reservation_state(self, reservation, reservation_data): + if reservation_data.toAssign is not None and not reservation_data.toAssign: + reservation.action_assign() + if reservation_data.stateCode == "cancel": + reservation.action_cancel() + if reservation_data.stateCode == "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) + line_vals = {} + if origin_data.price and ( + not reservation_line + or round(origin_data.price, 2) != round(reservation_line.price, 2) + ): + line_vals["price"] = origin_data.price + if origin_data.discount is not None and ( + not reservation_line + or round(origin_data.discount, 2) != round(reservation_line.discount, 2) + ): + line_vals["discount"] = origin_data.discount + if origin_data.cancelDiscount is not None and ( + not reservation_line + or round(origin_data.cancelDiscount, 2) + != round(reservation_line.cancelDiscount, 2) + ): + line_vals["cancel_discount"] = origin_data.cancelDiscount + if origin_data.roomId and ( + not reservation_line or origin_data.roomId != reservation_line.room_id.id + ): + line_vals["room_id"] = origin_data.roomId + return line_vals + + # ------------------------------------------------------------------------------------ + # RESERVATION LINES------------------------------------------------------------------- + # ------------------------------------------------------------------------------------ + + @restapi.method( + [ + ( + [ + "//reservation-lines", + ], + "GET", + ) + ], + output_param=Datamodel("pms.reservation.line.info", is_list=True), + auth="jwt_api_pms", + ) + def get_reservation_line(self, reservation_id): + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + if not reservation: + raise MissingError(_("Reservation not found")) + result_lines = [] + PmsReservationLineInfo = self.env.datamodels["pms.reservation.line.info"] + for reservation_line in reservation.reservation_line_ids: + result_lines.append( + PmsReservationLineInfo( + id=reservation_line.id, + date=datetime.combine( + reservation_line.date, datetime.min.time() + ).isoformat(), + price=round(reservation_line.price, 2), + discount=round(reservation_line.discount, 2), + cancelDiscount=round(reservation_line.cancel_discount, 2), + roomId=reservation_line.room_id.id, + reservationId=reservation_line.reservation_id.id, + pmsPropertyId=reservation_line.pms_property_id.id, + isReselling=reservation_line.is_reselling, + ) + ) + return result_lines + + @restapi.method( + [ + ( + [ + "//reservation-lines", + ], + "POST", + ) + ], + input_param=Datamodel("pms.reservation.line.info", is_list=False), + auth="jwt_api_pms", + ) + def create_reservation_line(self, reservation_id, reservation_line_info): + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + date = datetime.strptime(reservation_line_info.date, "%Y-%m-%d").date() + if not reservation: + raise MissingError(_("Reservation not found")) + if not reservation_line_info.date or not reservation_line_info.price: + raise MissingError(_("Date and price are required")) + if ( + date != reservation.checkin - timedelta(days=1) + and date != reservation.checkout + ): + raise MissingError( + _("It is only allowed to create contiguous nights to the reservation") + ) + vals = dict() + vals.update( + { + "reservation_id": reservation.id, + "date": date, + "price": reservation_line_info.price, + "room_id": reservation_line_info.roomId + if reservation_line_info.roomId + else reservation.preferred_room_id.id, + } + ) + self.env["pms.reservation.line"].create(vals) + + @restapi.method( + [ + ( + [ + "//reservation-lines/", + ], + "DELETE", + ) + ], + auth="jwt_api_pms", + ) + def delete_reservation_line(self, reservation_id, reservation_line_id): + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + line = reservation.reservation_line_ids.filtered( + lambda l: l.id == reservation_line_id + ) + if line and ( + line.date == min(reservation.reservation_line_ids.mapped("date")) + or line.date == max(reservation.reservation_line_ids.mapped("date")) + ): + line.unlink() + else: + raise MissingError(_("It was not possible to remove the reservation line")) + + @restapi.method( + [ + ( + [ + "/p//reservation-lines/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.reservation.line.info", is_list=False), + auth="jwt_api_pms", + ) + def update_reservation_lines( + self, _reservation_id, reservation_line_id, reservation_line_param + ): + if reservation_line_param.roomId: + reservation_line_id = self.env["pms.reservation.line"].browse( + reservation_line_id + ) + reservation_line_id.room_id = reservation_line_param.roomId + + # ------------------------------------------------------------------------------------ + # RESERVATION SERVICES---------------------------------------------------------------- + # ------------------------------------------------------------------------------------ + + @restapi.method( + [ + ( + [ + "//services", + ], + "GET", + ) + ], + output_param=Datamodel("pms.service.info", is_list=True), + auth="jwt_api_pms", + ) + def get_reservation_services(self, reservation_id): + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + if not reservation: + raise MissingError(_("Reservation not found")) + + result_services = [] + PmsServiceInfo = self.env.datamodels["pms.service.info"] + for service in reservation.service_ids: + PmsServiceLineInfo = self.env.datamodels["pms.service.line.info"] + service_lines = [] + for line in service.service_line_ids: + service_lines.append( + PmsServiceLineInfo( + id=line.id, + date=datetime.combine( + line.date, datetime.min.time() + ).isoformat(), + priceUnit=line.price_unit, + discount=line.discount, + quantity=line.day_qty, + ) + ) + + result_services.append( + PmsServiceInfo( + id=service.id, + reservationId=service.reservation_id, + name=service.name or service.product_id.name, + productId=service.product_id.id, + quantity=service.product_qty, + priceTotal=round(service.price_total, 2), + priceSubtotal=round(service.price_subtotal, 2), + priceTaxes=round(service.price_tax, 2), + discount=round(service.discount, 2), + isBoardService=service.is_board_service, + serviceLines=service_lines, + isCancelPenalty=service.is_cancel_penalty, + boardServiceLineId=service.board_service_line_id.id + if service.board_service_line_id + else None, + ) + ) + return result_services + + @restapi.method( + [ + ( + [ + "//services", + ], + "POST", + ) + ], + input_param=Datamodel("pms.service.info", is_list=False), + auth="jwt_api_pms", + ) + def create_reservation_service(self, reservation_id, service_info): + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + if not reservation: + raise MissingError(_("Reservation not found")) + vals = { + "product_id": service_info.productId, + "reservation_id": reservation.id, + "is_board_service": service_info.isBoardService or False, + } + skip_compute_service_line_ids = False + if service_info.serviceLines: + skip_compute_service_line_ids = True + vals["service_line_ids"] = [ + ( + 0, + False, + { + "date": line.date, + "price_unit": line.priceUnit, + "discount": line.discount or 0, + "day_qty": line.quantity, + }, + ) + for line in service_info.serviceLines + ] + service = ( + self.env["pms.service"] + .with_context(skip_compute_service_line_ids=skip_compute_service_line_ids) + .create(vals) + ) + + return service.id + + # ------------------------------------------------------------------------------------ + # RESERVATION CHECKINS---------------------------------------------------------------- + # ------------------------------------------------------------------------------------ + + @restapi.method( + [ + ( + [ + "//checkin-partners", + ], + "GET", + ) + ], + output_param=Datamodel("pms.checkin.partner.info", is_list=True), + auth="jwt_api_pms", + ) + def get_checkin_partners(self, reservation_id): + reservation = self.env["pms.reservation"].browse(reservation_id) + checkin_partners = [] + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] + if not reservation: + pass + else: + # TODO Review state draft + # .filtered( + # lambda ch: ch.state != "dummy" + # ) + for checkin_partner in reservation.checkin_partner_ids: + checkin_partners.append( + PmsCheckinPartnerInfo( + id=checkin_partner.id, + reservationId=checkin_partner.reservation_id.id, + partnerId=checkin_partner.partner_id.id + if checkin_partner.partner_id + else None, + name=checkin_partner.name if checkin_partner.name else "", + firstname=checkin_partner.firstname + if checkin_partner.firstname + else None, + lastname=checkin_partner.lastname + if checkin_partner.lastname + else None, + lastname2=checkin_partner.lastname2 + if checkin_partner.lastname2 + else None, + email=checkin_partner.email if checkin_partner.email else "", + mobile=checkin_partner.mobile if checkin_partner.mobile else "", + documentType=checkin_partner.document_type.id + if checkin_partner.document_type + else None, + documentNumber=checkin_partner.document_number + if checkin_partner.document_number + else None, + documentExpeditionDate=datetime.combine( + checkin_partner.document_expedition_date, + datetime.min.time(), + ).isoformat() + if checkin_partner.document_expedition_date + else None, + documentSupportNumber=checkin_partner.support_number + if checkin_partner.support_number + else None, + documentCountryId=checkin_partner.document_country_id.id + if checkin_partner.document_country_id + else None, + gender=checkin_partner.gender if checkin_partner.gender else "", + birthdate=datetime.combine( + checkin_partner.birthdate_date, datetime.min.time() + ).isoformat() + if checkin_partner.birthdate_date + else None, + residenceStreet=checkin_partner.residence_street + if checkin_partner.residence_street + else None, + zip=checkin_partner.residence_zip + if checkin_partner.residence_zip + else None, + residenceCity=checkin_partner.residence_city + if checkin_partner.residence_city + else None, + nationality=checkin_partner.nationality_id.id + if checkin_partner.nationality_id + else None, + countryState=checkin_partner.residence_state_id.id + if checkin_partner.residence_state_id + else None, + countryStateName=checkin_partner.residence_state_id.name + if checkin_partner.residence_state_id + else None, + countryId=checkin_partner.residence_country_id.id + if checkin_partner.residence_country_id + else None, + checkinPartnerState=checkin_partner.state, + signature=checkin_partner.signature + if checkin_partner.signature + else None, + relationship=checkin_partner.ses_partners_relationship + if checkin_partner.ses_partners_relationship + else "", + responsibleCheckinPartnerId=checkin_partner.ses_related_checkin_partner_id.id + if checkin_partner.ses_related_checkin_partner_id + else None, + ) + ) + return checkin_partners + + @restapi.method( + [ + ( + [ + "/p//checkin-partners/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.checkin.partner.info", is_list=False), + auth="jwt_api_pms", + ) + def write_reservation_checkin_partner( + self, reservation_id, checkin_partner_id, pms_checkin_partner_info + ): + checkin_partner = self.env["pms.checkin.partner"].search( + [("id", "=", checkin_partner_id), ("reservation_id", "=", reservation_id)] + ) + if not checkin_partner: + raise MissingError(_("Checkin partner not found")) + if ( + pms_checkin_partner_info.actionOnBoard + and pms_checkin_partner_info.actionOnBoard is not None + ): + checkin_partner.action_on_board() + return checkin_partner.id + if not pms_checkin_partner_info.originInputData: + pms_checkin_partner_info.originInputData = checkin_partner.origin_input_data + checkin_partner.write( + self.mapping_checkin_partner_values( + pms_checkin_partner_info, + checkin_partner.partner_id.id if checkin_partner.partner_id else False, + ) + ) + if pms_checkin_partner_info.responsibleCheckinPartnerId: + responsible_checkin_partner_record = ( + self.env["pms.checkin.partner"] + .search( + [("id", "=", pms_checkin_partner_info.responsibleCheckinPartnerId)] + ) + ) + if responsible_checkin_partner_record: + responsible_checkin_partner_record.ses_partners_relationship = ( + pms_checkin_partner_info.relationship + ) + + # if not partner_id we need to force compute to create partner + if not checkin_partner.partner_id: + checkin_partner._compute_partner_id() + + return checkin_partner.id + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.reservation.search.param", is_list=False), + output_param=Datamodel("pms.reservation.info", is_list=True), + auth="jwt_api_pms", + ) + def get_reservations(self, pms_search_param): + domain = list() + res_reservations = [] + if pms_search_param.pmsPropertyId: + domain.append(("pms_property_id", "=", pms_search_param.pmsPropertyId)) + if pms_search_param.toAssign: + domain.append(("to_assign", "=", True)) + domain.append(("checkin", ">=", fields.Date.today())) + if pms_search_param.ids: + domain.append(("id", "in", pms_search_param.ids)) + if pms_search_param.createDateFrom and pms_search_param.createDateTo: + domain.append( + ( + "create_date", + ">=", + datetime.strptime( + pms_search_param.createDateFrom, "%Y-%m-%d %H:%M:%S" + ), + ) + ) + domain.append( + ( + "create_date", + "<=", + datetime.strptime( + pms_search_param.createDateTo, "%Y-%m-%d %H:%M:%S" + ), + ) + ) + if pms_search_param.lastUpdateFrom: + last_update = fields.Datetime.from_string(pms_search_param.lastUpdateFrom) + domain.append(("write_date", ">=", last_update)) + + reservations = self.env["pms.reservation"].search(domain) + PmsReservationInfo = self.env.datamodels["pms.reservation.info"] + if not reservations: + pass + else: + for reservation in reservations: + res_reservations.append( + PmsReservationInfo( + id=reservation.id, + name=reservation.name, + folioId=reservation.folio_id.id, + folioSequence=reservation.folio_sequence, + partnerId=reservation.partner_id.id + if reservation.partner_id + else None, + partnerName=reservation.partner_name or None, + boardServiceId=reservation.board_service_room_id.id + if reservation.board_service_room_id + else None, + saleChannelId=reservation.sale_channel_origin_id.id + if reservation.sale_channel_origin_id + else None, + agencyId=reservation.agency_id.id + if reservation.agency_id + else None, + userId=reservation.user_id.id if reservation.user_id else None, + checkin=datetime.combine( + reservation.checkin, datetime.min.time() + ).isoformat(), + checkout=datetime.combine( + reservation.checkout, datetime.min.time() + ).isoformat(), + arrivalHour=reservation.arrival_hour, + departureHour=reservation.departure_hour, + roomTypeId=reservation.room_type_id.id + if reservation.room_type_id + else None, + preferredRoomId=reservation.preferred_room_id.id + if reservation.preferred_room_id + else None, + pricelistId=reservation.pricelist_id.id + if reservation.pricelist_id + else None, + adults=reservation.adults if reservation.adults else None, + overbooking=reservation.overbooking, + externalReference=reservation.external_reference + if reservation.external_reference + else None, + stateCode=reservation.state, + stateDescription=dict( + reservation.fields_get(["state"])["state"]["selection"] + )[reservation.state], + children=reservation.children if reservation.children else None, + readyForCheckin=reservation.ready_for_checkin, + allowedCheckout=reservation.allowed_checkout, + isSplitted=reservation.splitted, + pendingCheckinData=reservation.pending_checkin_data, + createDate=reservation.create_date.isoformat(), + segmentationId=reservation.segmentation_ids[0].id + if reservation.segmentation_ids + else None, + toAssign=reservation.to_assign, + reservationType=reservation.reservation_type, + priceTotal=round(reservation.price_room_services_set, 2), + discount=round(reservation.discount, 2), + commissionAmount=round(reservation.commission_amount, 2) + if reservation.commission_amount + else None, + priceOnlyServices=round(reservation.price_services, 2), + priceOnlyRoom=round(reservation.price_total, 2), + ) + ) + return res_reservations + + @restapi.method( + [ + ( + [ + "/partner-as-host", + ], + "GET", + ) + ], + input_param=Datamodel("pms.partner.search.param", is_list=False), + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="jwt_api_pms", + ) + def get_reservations_for_partners_as_host(self, pms_partner_search_param): + checkins = self.env["pms.checkin.partner"].search( + [("partner_id", "=", pms_partner_search_param.id)] + ) + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + reservations = [] + if checkins: + for checkin in checkins: + reservation = self.env["pms.reservation"].search( + [("id", "=", checkin.reservation_id.id)] + ) + + reservations.append( + PmsReservationShortInfo( + id=reservation.id, + checkin=reservation.checkin.strftime("%d/%m/%Y"), + checkout=reservation.checkout.strftime("%d/%m/%Y"), + adults=reservation.adults, + priceTotal=round(reservation.price_room_services_set, 2), + stateCode=reservation.state, + paymentState=reservation.folio_payment_state, + ) + ) + return reservations + + @restapi.method( + [ + ( + [ + "/partner-as-customer", + ], + "GET", + ) + ], + input_param=Datamodel("pms.partner.search.param", is_list=False), + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="jwt_api_pms", + ) + def get_reservations_for_partner_as_customer(self, pms_partner_search_param): + partnerReservations = self.env["pms.reservation"].search( + [("partner_id", "=", pms_partner_search_param.id)] + ) + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + reservations = [] + for reservation in partnerReservations: + reservations.append( + PmsReservationShortInfo( + checkin=reservation.checkin.strftime("%d/%m/%Y"), + checkout=reservation.checkout.strftime("%d/%m/%Y"), + adults=reservation.adults, + priceTotal=round(reservation.price_room_services_set, 2), + stateCode=reservation.state, + paymentState=reservation.folio_payment_state, + ) + ) + return reservations + + @restapi.method( + [ + ( + [ + "//checkin-partners", + ], + "POST", + ) + ], + input_param=Datamodel("pms.checkin.partner.info", is_list=False), + auth="jwt_api_pms", + ) + def create_reservation_checkin_partner( + self, reservation_id, pms_checkin_partner_info + ): + reservation_rec = self.env["pms.reservation"].browse(reservation_id) + if any( + reservation_rec.checkin_partner_ids.filtered(lambda ch: ch.state == "dummy") + ): + checkin_partner_last_id = max( + reservation_rec.checkin_partner_ids.filtered( + lambda ch: ch.state == "dummy" + ) + ).id + checkin_partner = self.env["pms.checkin.partner"].browse( + checkin_partner_last_id + ) + checkin_partner.write( + self.mapping_checkin_partner_values( + pms_checkin_partner_info, + checkin_partner.partner_id.id + if checkin_partner.partner_id + else False, + ) + ) + # if not partner_id we need to force compute to create partner + if not checkin_partner.partner_id: + checkin_partner._compute_partner_id() + return checkin_partner.id + + @restapi.method( + [ + ( + [ + "//checkin-partners/", + ], + "DELETE", + ) + ], + auth="jwt_api_pms", + ) + def delete_reservation_checkin_partner(self, reservation_id, checkin_partner_id): + checkin_partner = self.env["pms.checkin.partner"].browse(checkin_partner_id) + if checkin_partner: + checkin_partner.unlink() + + 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, + "lastname2": pms_checkin_partner_info.lastname2, + "email": pms_checkin_partner_info.email, + "mobile": pms_checkin_partner_info.mobile, + "document_type": pms_checkin_partner_info.documentType, + "document_number": pms_checkin_partner_info.documentNumber, + "document_country_id": pms_checkin_partner_info.documentCountryId, + "support_number": pms_checkin_partner_info.documentSupportNumber, + "gender": pms_checkin_partner_info.gender, + "residence_street": pms_checkin_partner_info.residenceStreet, + "nationality_id": pms_checkin_partner_info.nationality, + "residence_zip": pms_checkin_partner_info.zip, + "residence_city": pms_checkin_partner_info.residenceCity, + "residence_state_id": pms_checkin_partner_info.countryState, + "residence_country_id": pms_checkin_partner_info.countryId, + "origin_input_data": pms_checkin_partner_info.originInputData, + } + if pms_checkin_partner_info.partnerId != partner_id: + vals.update({"partner_id": pms_checkin_partner_info.partnerId}) + if pms_checkin_partner_info.documentExpeditionDate: + document_expedition_date = datetime.strptime( + pms_checkin_partner_info.documentExpeditionDate, "%d/%m/%Y" + ) + document_expedition_date = document_expedition_date.strftime("%Y-%m-%d") + vals.update({"document_expedition_date": document_expedition_date}) + else: + vals.update({"document_expedition_date": False}) + if pms_checkin_partner_info.birthdate: + birthdate = datetime.strptime( + pms_checkin_partner_info.birthdate, "%d/%m/%Y" + ) + birthdate = birthdate.strftime("%Y-%m-%d") + vals.update({"birthdate_date": birthdate}) + else: + vals.update({"birthdate_date": False}) + if pms_checkin_partner_info.signature: + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(base64.b64decode(pms_checkin_partner_info.signature)) + temp_path = f.name + + with open(temp_path, "rb") as f: + signature_image = f.read() + os.unlink(temp_path) + + vals.update({"signature": base64.b64encode(signature_image)}) + else: + vals.update({"signature": False}) + if pms_checkin_partner_info.relationship != "": + vals.update( + { + "ses_partners_relationship": find_opposite_relationship( + pms_checkin_partner_info.relationship, + ) + } + ) + if pms_checkin_partner_info.responsibleCheckinPartnerId: + vals.update( + { + "ses_related_checkin_partner_id": pms_checkin_partner_info.responsibleCheckinPartnerId + } + ) + return vals + + @restapi.method( + [ + ( + [ + "//checkin-report", + ], + "GET", + ) + ], + auth="jwt_api_pms", + output_param=Datamodel("pms.report", is_list=False), + ) + def print_all_checkins(self, reservation_id): + reservations = False + if reservation_id: + reservations = self.env["pms.reservation"].sudo().browse(reservation_id) + checkins = reservations.checkin_partner_ids.filtered( + lambda x: x.state in ["precheckin", "onboard", "done"] + ) + pdf = ( + self.env.ref("pms.action_traveller_report") + .sudo() + ._render_qweb_pdf(checkins.ids)[0] + ) + base64EncodedStr = base64.b64encode(pdf) + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(binary=base64EncodedStr) + + @restapi.method( + [ + ( + [ + "//checkin-partners/" + "/checkin-report", + ], + "GET", + ) + ], + auth="jwt_api_pms", + output_param=Datamodel("pms.report", is_list=False), + ) + def print_checkin(self, reservation_id, checkin_partner_id): + reservations = False + if reservation_id: + reservations = self.env["pms.reservation"].sudo().browse(reservation_id) + checkin_partner = reservations.checkin_partner_ids.filtered( + lambda x: x.id == checkin_partner_id + ) + pdf = ( + self.env.ref("pms.action_traveller_report") + .sudo() + ._render_qweb_pdf(checkin_partner.id)[0] + ) + base64EncodedStr = base64.b64encode(pdf) + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(binary=base64EncodedStr) + + @restapi.method( + [ + ( + [ + "/kelly-report", + ], + "GET", + ) + ], + input_param=Datamodel("pms.report.search.param", is_list=False), + output_param=Datamodel("pms.report", is_list=False), + auth="jwt_api_pms", + ) + def kelly_report(self, pms_report_search_param): + pms_property_id = pms_report_search_param.pmsPropertyId + date_from = fields.Date.from_string(pms_report_search_param.dateFrom) + + report_wizard = self.env["kellysreport"].create( + { + "date_start": date_from, + "pms_property_id": pms_property_id, + } + ) + report_wizard.calculate_report() + result = report_wizard._excel_export() + file_name = result["xls_filename"] + base64EncodedStr = result["xls_binary"] + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(fileName=file_name, binary=base64EncodedStr) + + @restapi.method( + [ + ( + [ + "/arrivals-report", + ], + "GET", + ) + ], + input_param=Datamodel("pms.report.search.param", is_list=False), + output_param=Datamodel("pms.report", is_list=False), + auth="jwt_api_pms", + ) + def arrivals_report(self, pms_report_search_param): + pms_property_id = pms_report_search_param.pmsPropertyId + date_from = fields.Date.from_string(pms_report_search_param.dateFrom) + + query = self.env.ref("pms_api_rest.sql_export_arrivals") + if not query: + raise MissingError(_("SQL query not found")) + report_wizard = self.env["sql.file.wizard"].create({"sql_export_id": query.id}) + if not report_wizard._fields.get( + "x_date_from" + ) or not report_wizard._fields.get("x_pms_property_id"): + raise MissingError( + _("The Query params was modifieds, please contact the administrator") + ) + report_wizard.x_date_from = date_from + report_wizard.x_pms_property_id = pms_property_id + + report_wizard.export_sql() + file_name = report_wizard.file_name + base64EncodedStr = report_wizard.binary_file + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(fileName=file_name, binary=base64EncodedStr) + + @restapi.method( + [ + ( + [ + "/departures-report", + ], + "GET", + ) + ], + input_param=Datamodel("pms.report.search.param", is_list=False), + output_param=Datamodel("pms.report", is_list=False), + auth="jwt_api_pms", + ) + def departures_report(self, pms_report_search_param): + pms_property_id = pms_report_search_param.pmsPropertyId + date_from = fields.Date.from_string(pms_report_search_param.dateFrom) + + query = self.env.ref("pms_api_rest.sql_export_departures") + if not query: + raise MissingError(_("SQL query not found")) + if query.state == "draft": + query.button_validate_sql_expression() + report_wizard = self.env["sql.file.wizard"].create({"sql_export_id": query.id}) + if not report_wizard._fields.get( + "x_date_from" + ) or not report_wizard._fields.get("x_pms_property_id"): + raise MissingError( + _("The Query params was modifieds, please contact the administrator") + ) + report_wizard.x_date_from = date_from + report_wizard.x_pms_property_id = pms_property_id + + report_wizard.export_sql() + file_name = report_wizard.file_name + base64EncodedStr = report_wizard.binary_file + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(fileName=file_name, binary=base64EncodedStr) + + @restapi.method( + [ + ( + [ + "//wizard-states", + ], + "GET", + ) + ], + output_param=Datamodel("pms.wizard.state.info", is_list=False), + auth="jwt_api_pms", + ) + def wizard_states(self, reservation_id): + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + 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'])" + "]", + "filtered": "lambda r: r.count_alternative_free_rooms", + "text": f"Parece que ha entrado una reserva sin haber disponibilidad para {reservation.sudo().room_type_id.name}.", + "priority": 100, + }, + { + "code": "overbooking_without_availability", + "title": "Overbooking", + "domain": "[" + "('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", + "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'])" + "]", + "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. ", + "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'])" + "]", + "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'}.", + "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}')," + "]", + "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í.", + "priority": 300, + }, + { + "code": "to_confirm", + "title": "Por confirmar", + "domain": "[('state', '=', 'draft')," + 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, + }, + { + "code": "checkin_done_precheckin", + "title": "Entrada Hoy", + "domain": "[('state', '=', 'confirm')," + 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í", + "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'])" + "]", + "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.", + "priority": 530, + }, + { + "code": "checkin_no_precheckin", + "title": "Entrada Hoy", + "domain": "[('state', '=', 'confirm')," + 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, + }, + { + "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'])" + "]", + "text": "Esta reserva está pendiente de cobro y de que los huéspedes " + " 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'])" + "]", + "text": "Esta reserva está pendiente de cobro, puedes enviarle sun recordatorio desde aquí", + "priority": 630, + }, + { + "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'])" + "]", + "text": "Esta reserva no tiene los datos de los huéspedes registrados, puedes enviarles un recordatorio desde aquí", + "priority": 660, + }, + { + "code": "cancelled", + "title": "Cancelada con cargos y sin cobrar", + "domain": "[('state', '=', 'cancel')," + "('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.", + "priority": 700, + }, + { + "code": "onboard_without_payment", + "title": "Por cobrar dentro", + "domain": "[('state', 'in', ['onboard', 'departure_delayed'])," + "('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, + }, + { + "code": "done_without_payment", + "title": "Por cobrar pasadas", + "domain": "[('state', '=', 'done')," + "('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í.", + "priority": 900, + }, + { + "code": "checkout", + "title": "Checkout", + "domain": "[('state', 'in', ['onboard', 'departure_delayed'])," + 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. + + 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"])] + ) + 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"])): + continue + + return PmsWizardStateInfo( + code=state["code"], + title=state["title"], + text=state["text"], + ) + + return PmsWizardStateInfo( + code="", + title="", + text="", + ) + + # PUBLIC ENDPOINTS + @restapi.method( + [ + ( + [ + "//precheckin/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.folio.public.info", is_list=False), + auth="public", + ) + def get_reservation_public_info(self, reservation_id, token): + # variable initialization + folio_room_types_description_list = list() + folio_checkin_partner_names = list() + num_checkins = 0 + + # check if the folio exists + reservation_record = self.env["pms.reservation"].sudo().browse(reservation_id) + if not reservation_record.exists(): + raise MissingError(_("Reservation not found")) + + # check if the reservation is accessible + try: + reservation_record = CustomerPortal._document_check_access( + self, + "pms.reservation", + reservation_id, + access_token=token, + ) + except AccessError: + raise MissingError(_("Reservation not found")) + + reservation_checkin_partner_names = [] + reservation_checkin_partners = [] + num_checkins += len(reservation_record.checkin_partner_ids) + folio_room_types_description_list.append(reservation_record.room_type_id.name) + + # iterate checkin partner names completed + for checkin_partner in reservation_record.checkin_partner_ids: + reservation_checkin_partners.append( + self.env.datamodels["pms.checkin.partner.info"]( + id=checkin_partner.id, + checkinPartnerState=checkin_partner.state, + ) + ) + is_mandatory_fields = True + for field in self.env["pms.checkin.partner"]._checkin_mandatory_fields(): + if not getattr(checkin_partner, field): + is_mandatory_fields = False + break + if is_mandatory_fields: + reservation_checkin_partner_names.append(checkin_partner.firstname) + folio_checkin_partner_names.append(checkin_partner.firstname) + + # append reservation public info + reservations = [ + self.env.datamodels["pms.reservation.public.info"]( + roomTypeName=reservation_record.room_type_id.name, + checkinNamesCompleted=reservation_checkin_partner_names, + nights=reservation_record.nights, + checkin=datetime.combine( + reservation_record.checkin, datetime.min.time() + ).isoformat(), + checkout=datetime.combine( + reservation_record.checkout, datetime.min.time() + ).isoformat(), + adults=reservation_record.adults, + children=reservation_record.children, + reservationReference=reservation_record.name, + checkinPartners=reservation_checkin_partners, + reservationAmount=reservation_record.price_total, + ) + ] + ine_category = "" + if reservation_record.pms_property_id.ine_category_id: + ine_category = ( + reservation_record.pms_property_id.ine_category_id.category + + " (" + + reservation_record.pms_property_id.ine_category_id.type + + ")" + ) + + return self.env.datamodels["pms.folio.public.info"]( + pmsPropertyName=reservation_record.pms_property_id.name, + pmsPropertyStreet=reservation_record.pms_property_id.street, + pmsPropertyCity=reservation_record.pms_property_id.city, + pmsPropertyZip=reservation_record.pms_property_id.zip, + pmsPropertyState=reservation_record.pms_property_id.state_id.name, + pmsPropertyPhoneNumber=reservation_record.pms_property_id.phone, + pmsPropertyLogo=url_image_pms_api_rest( + "pms.property", + reservation_record.pms_property_id.id, + "logo", + ), + pmsPropertyIneCategory=ine_category, + pmsPropertyImage=url_image_pms_api_rest( + "pms.property", + reservation_record.pms_property_id.id, + "hotel_image_pms_api_rest", + ), + pmsPropertyIsOCRAvailable=True + if reservation_record.pms_property_id.ocr_checkin_supplier + else False, + pmsPropertyPrivacyPolicy=remove_html_tags( + reservation_record.pms_property_id.privacy_policy + ) + if reservation_record.pms_property_id.privacy_policy + else "", + pmsCompanyName=reservation_record.pms_property_id.company_id.name, + pmsPropertyId=reservation_record.pms_property_id.id, + folioPartnerName=reservation_record.folio_id.partner_name, + reservations=reservations, + cardexWarning=reservation_record.pms_property_id.cardex_warning + if reservation_record.pms_property_id.cardex_warning + else "", + ) + + @restapi.method( + [ + ( + [ + "//precheckin-reservation/" + "/partner//", + ], + "GET", + ) + ], + output_param=Datamodel("pms.partner.info", is_list=True), + auth="public", + ) + def get_checkin_partner_by_doc_number( + self, reservation_id, token, document_type, document_number + ): + reservation_record = self.env["pms.reservation"].sudo().browse(reservation_id) + if not reservation_record: + raise MissingError(_("Folio not found")) + # check if the reservation is accessible + try: + CustomerPortal._document_check_access( + self, + "pms.reservation", + reservation_record.id, + access_token=token, + ) + except AccessError: + raise MissingError(_("Reservation not found")) + + doc_type = ( + self.env["res.partner.id_category"] + .sudo() + .search([("id", "=", document_type)]) + ) + # Clean Document number + document_number = re.sub(r"[^a-zA-Z0-9]", "", document_number).upper() + partner = ( + self.env["pms.checkin.partner"] + .sudo() + ._get_partner_by_document(document_number, doc_type) + ) + partners = [] + if partner: + doc_record = partner.id_numbers.filtered( + lambda doc: doc.category_id.id == doc_type.id + ) + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] + + document_numbers_in_reservation = ( + reservation_record.checkin_partner_ids.filtered( + lambda x: x.document_type.id == doc_type.id + and x.document_number == document_number + ) + ) + + partners.append( + PmsCheckinPartnerInfo( + # partner id + partnerId=partner.id, + # names + firstname="#" if partner.firstname else None, + lastname="#" if partner.lastname else None, + lastname2="#" if partner.lastname2 else None, + # contact + email="#" if partner.email else None, + mobile="#" if partner.mobile else None, + # document info + documentCountryId=doc_record.country_id.id + if doc_record.country_id.id + else None, + documentType=doc_type.id if doc_type.id else None, + documentNumber="#" if doc_record.name else None, + documentExpeditionDate=datetime.utcfromtimestamp(0).isoformat() + if doc_record.valid_from + else None, + documentSupportNumber="#" if doc_record.support_number else None, + # personal info + gender="#" if partner.gender else None, + birthdate=datetime.utcfromtimestamp(0).isoformat() + if partner.birthdate_date + else None, + # nationality + nationality=-1 if partner.nationality_id.id else None, + # residence info + countryId=partner.residence_country_id + if partner.residence_country_id + else None, + residenceStreet="#" if partner.residence_street else None, + zip="#" if partner.residence_zip else None, + residenceCity="#" if partner.residence_city else None, + countryState=-1 if partner.residence_state_id.id else None, + # is already in reservation + isAlreadyInReservation=True + if document_numbers_in_reservation + else False, + ) + ) + return partners + + @restapi.method( + [ + ( + [ + "//precheckin-reservation/" + "/checkin-partners/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.checkin.partner.info", is_list=False), + auth="public", + ) + def patch_checkin_partner( + self, reservation_id, token, checkin_partner_id, pms_checkin_partner_info + ): + reservation_record = self.env["pms.reservation"].sudo().browse(reservation_id) + if not reservation_record: + raise MissingError(_("Folio not found")) + # check if the reservation is accessible + try: + CustomerPortal._document_check_access( + self, + "pms.reservation", + reservation_record.id, + access_token=token, + ) + except AccessError: + raise MissingError(_("Reservation not found")) + + partner_record = False + # search checkin partner by id + checkin_partner_record = ( + self.env["pms.checkin.partner"].sudo().browse(checkin_partner_id) + ) + if pms_checkin_partner_info.partnerId: + # search partner by api_rest_id + partner_record = ( + self.env["res.partner"] + .sudo() + .browse(pms_checkin_partner_info.partnerId) + ) + + # partner + if partner_record: + checkin_partner_record.partner_id = partner_record.id + # document info + if pms_checkin_partner_info.documentCountryId: + checkin_partner_record.document_country_id = ( + pms_checkin_partner_info.documentCountryId + ) + if ( + pms_checkin_partner_info.documentNumber + and pms_checkin_partner_info.documentType + ): + checkin_partner_record.write( + { + "document_type": pms_checkin_partner_info.documentType, + "document_number": pms_checkin_partner_info.documentNumber, + } + ) + if pms_checkin_partner_info.documentExpeditionDate: + checkin_partner_record.document_expedition_date = ( + pms_checkin_partner_info.documentExpeditionDate + ) + if pms_checkin_partner_info.documentSupportNumber: + checkin_partner_record.support_number = ( + pms_checkin_partner_info.documentSupportNumber + ) + # origin input data + if pms_checkin_partner_info.originInputData: + checkin_partner_record.origin_input_data = ( + pms_checkin_partner_info.originInputData + ) + # name + if pms_checkin_partner_info.firstname: + checkin_partner_record.firstname = pms_checkin_partner_info.firstname + if pms_checkin_partner_info.lastname: + checkin_partner_record.lastname = pms_checkin_partner_info.lastname + if pms_checkin_partner_info.lastname2: + checkin_partner_record.lastname2 = pms_checkin_partner_info.lastname2 + # personal info + if pms_checkin_partner_info.birthdate: + checkin_partner_record.birthdate_date = pms_checkin_partner_info.birthdate + if pms_checkin_partner_info.gender: + checkin_partner_record.gender = pms_checkin_partner_info.gender + # nationality + if pms_checkin_partner_info.nationality: + checkin_partner_record.nationality_id = pms_checkin_partner_info.nationality + # residence info + if pms_checkin_partner_info.countryId: + checkin_partner_record.residence_country_id = ( + pms_checkin_partner_info.countryId + ) + if pms_checkin_partner_info.zip: + checkin_partner_record.residence_zip = pms_checkin_partner_info.zip + if pms_checkin_partner_info.residenceCity: + checkin_partner_record.residence_city = ( + pms_checkin_partner_info.residenceCity + ) + if pms_checkin_partner_info.countryState: + checkin_partner_record.residence_state_id = ( + pms_checkin_partner_info.countryState + ) + if pms_checkin_partner_info.residenceStreet: + checkin_partner_record.residence_street = ( + pms_checkin_partner_info.residenceStreet + ) + # contact + if pms_checkin_partner_info.email: + checkin_partner_record.email = pms_checkin_partner_info.email + if pms_checkin_partner_info.mobile: + checkin_partner_record.mobile = pms_checkin_partner_info.mobile + # signature + if pms_checkin_partner_info.signature: + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(base64.b64decode(pms_checkin_partner_info.signature)) + temp_path = f.name + + with open(temp_path, "rb") as f: + signature_image = f.read() + os.unlink(temp_path) + + checkin_partner_record.signature = base64.b64encode(signature_image) + else: + checkin_partner_record.signature = False + # legal representative + if ( + pms_checkin_partner_info.documentLegalRepresentative + and pms_checkin_partner_info.relationship + ): + folio_id = ( + self.env["pms.reservation"] + .sudo() + .browse(pms_checkin_partner_info.reservationId) + .folio_id.id + ) + record_checkin_partner_legal_representative = ( + self.env["pms.checkin.partner"] + .sudo() + .search( + [ + ( + "document_number", + "=", + pms_checkin_partner_info.documentLegalRepresentative, + ), + ( + "reservation_id", + "folio_id", + "=", + pms_checkin_partner_info.reservationId, + folio_id, + ), + ], + limit=1, + ) + ) + if record_checkin_partner_legal_representative: + checkin_partner_record.write( + { + "ses_related_checkin_partner_id": record_checkin_partner_legal_representative.id, + "ses_partners_relationship": find_opposite_relationship( + pms_checkin_partner_info.relationship + ), + } + ) + record_checkin_partner_legal_representative.write( + { + "ses_partners_relationship": pms_checkin_partner_info.relationship, + } + ) + + @restapi.method( + [ + ( + [ + "//precheckin//folio-adults", + ], + "GET", + ) + ], + input_param=Datamodel("pms.checkin.partner.info", is_list=False), + auth="public", + ) + def are_there_adults_registered_in_folio( + self, reservation_id, token, checkin_partner_search_params + ): + # check if the reservation exists + reservation_record = self.env["pms.reservation"].sudo().browse(reservation_id) + if not reservation_record.exists(): + raise MissingError(_("Reservation not found")) + + # check if the reservation is accessible + try: + CustomerPortal._document_check_access( + self, + "pms.reservation", + reservation_record.id, + access_token=token, + ) + except AccessError: + raise MissingError(_("Folio not found")) + + if checkin_partner_search_params.documentNumber: + adults = reservation_record.folio_id.checkin_partner_ids.filtered( + lambda x: x.document_number + == checkin_partner_search_params.documentNumber + and is_adult(x.birthdate_date) + ) + if adults: + rdo = True + else: + rdo = False + else: + adults = reservation_record.folio_id.checkin_partner_ids.filtered( + lambda x: is_adult(x.birthdate_date) + ) + if adults: + rdo = True + else: + rdo = False + return rdo diff --git a/pms_api_rest/services/pms_room_closure_reason_service.py b/pms_api_rest/services/pms_room_closure_reason_service.py new file mode 100644 index 0000000000..0245acb690 --- /dev/null +++ b/pms_api_rest/services/pms_room_closure_reason_service.py @@ -0,0 +1,33 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsClosureReasonService(Component): + _inherit = "base.rest.service" + _name = "pms.closure.reason.service" + _usage = "room-closure-reasons" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.room.closure.reason.info", is_list=True), + auth="jwt_api_pms", + ) + def get_closure_reasons(self): + closure_reasons = [] + PmsRoomClosureReasonInfo = self.env.datamodels["pms.room.closure.reason.info"] + for cl in self.env["room.closure.reason"].search([]): + closure_reasons.append( + PmsRoomClosureReasonInfo( + id=cl.id, name=cl.name, description=cl.description or None + ) + ) + return closure_reasons diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py new file mode 100644 index 0000000000..9a62bc9a01 --- /dev/null +++ b/pms_api_rest/services/pms_room_service.py @@ -0,0 +1,202 @@ +from datetime import datetime + +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsRoomService(Component): + _inherit = "base.rest.service" + _name = "pms.room.service" + _usage = "rooms" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.room.search.param"), + output_param=Datamodel("pms.room.info", is_list=True), + auth="jwt_api_pms", + ) + def get_rooms(self, room_search_param): + domain = [] + if room_search_param.name: + domain.append(("name", "like", room_search_param.name)) + if room_search_param.pmsPropertyId: + domain.append(("pms_property_id", "=", room_search_param.pmsPropertyId)) + if ( + room_search_param.availabilityFrom + and room_search_param.availabilityTo + and room_search_param.pmsPropertyId + ): + date_from = datetime.strptime( + room_search_param.availabilityFrom, "%Y-%m-%d" + ).date() + date_to = datetime.strptime( + room_search_param.availabilityTo, "%Y-%m-%d" + ).date() + pms_property = self.env["pms.property"].browse( + room_search_param.pmsPropertyId + ) + if not room_search_param.pricelistId: + pms_property = self.env["pms.property"].browse( + room_search_param.pmsPropertyId + ) + pms_property = pms_property.with_context( + checkin=date_from, + checkout=date_to, + room_type_id=False, # Allows to choose any available room + current_lines=room_search_param.currentLines, + real_avail=True, + ) + else: + pms_property = pms_property.with_context( + checkin=date_from, + checkout=date_to, + room_type_id=False, # Allows to choose any available room + current_lines=room_search_param.currentLines, + pricelist_id=room_search_param.pricelistId, + real_avail=True, + ) + domain.append(("id", "in", pms_property.free_room_ids.ids)) + result_rooms = [] + PmsRoomInfo = self.env.datamodels["pms.room.info"] + for room in ( + self.env["pms.room"] + .search( + domain, + ) + .sorted("sequence") + ): + # TODO: avoid, change short_name, + # set code amenities like a tag in room calendar name? + short_name = room.short_name + # if room.room_amenity_ids: + # for amenity in room.room_amenity_ids: + # if amenity.is_add_code_room_name: + # short_name += "%s" % amenity.default_code + result_rooms.append( + PmsRoomInfo( + id=room.id, + name=room.display_name, + roomTypeId=room.room_type_id, + capacity=room.capacity, + shortName=short_name, + roomTypeClassId=room.room_type_id.class_id, + ubicationId=room.ubication_id, + extraBedsAllowed=room.extra_beds_allowed, + roomAmenityIds=room.room_amenity_ids.ids + if room.room_amenity_ids + else None, + roomAmenityInName=room.room_amenity_ids.filtered( + lambda x: x.is_add_code_room_name + ).default_code + if room.room_amenity_ids.filtered( + lambda x: x.is_add_code_room_name + ).name + else "", + ) + ) + return result_rooms + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.room.info", is_list=False), + auth="jwt_api_pms", + ) + def get_room(self, room_id): + room = self.env["pms.room"].search([("id", "=", room_id)]) + if room: + PmsRoomInfo = self.env.datamodels["pms.room.info"] + return PmsRoomInfo( + id=room.id, + name=room.name, + roomTypeId=room.room_type_id, + capacity=room.capacity, + shortName=room.short_name, + extraBedsAllowed=room.extra_beds_allowed, + ) + else: + raise MissingError(_("Room not found")) + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.room.info"), + auth="jwt_api_pms", + ) + def update_room(self, room_id, pms_room_info_data): + room = self.env["pms.room"].search([("id", "=", room_id)]) + room_vals = {} + if not room: + raise MissingError(_("Room not found")) + + if pms_room_info_data.name: + room_vals["name"] = pms_room_info_data.name + + if room_vals: + room.write(room_vals) + + @restapi.method( + [ + ( + [ + "/", + ], + "DELETE", + ) + ], + auth="jwt_api_pms", + ) + def delete_room(self, room_id): + # esto tb podría ser con un browse + room = self.env["pms.room"].search([("id", "=", room_id)]) + if room: + room.active = False + else: + raise MissingError(_("Room not found")) + + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.room.info"), + auth="jwt_api_pms", + ) + def create_room(self, pms_room_info_param): + room = self.env["pms.room"].create( + { + "name": pms_room_info_param.name, + "room_type_id": pms_room_info_param.roomTypeId, + "capacity": pms_room_info_param.capacity, + "short_name": pms_room_info_param.shortName, + } + ) + return room.id diff --git a/pms_api_rest/services/pms_room_type_class_service.py b/pms_api_rest/services/pms_room_type_class_service.py new file mode 100644 index 0000000000..8e9d5498ca --- /dev/null +++ b/pms_api_rest/services/pms_room_type_class_service.py @@ -0,0 +1,61 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +from ..pms_api_rest_utils import url_image_pms_api_rest + + +class PmsRoomTypeClassService(Component): + _inherit = "base.rest.service" + _name = "pms.room.type.class.service" + _usage = "room-type-classes" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.room.type.class.search.param"), + output_param=Datamodel("pms.room.type.class.info", is_list=True), + auth="jwt_api_pms", + ) + def get_room_type_class(self, room_type_class_search_param): + if room_type_class_search_param.pmsPropertyIds: + room_type_classes = ( + self.env["pms.room"] + .search( + [ + ( + "pms_property_id", + "in", + room_type_class_search_param.pmsPropertyIds, + ) + ] + ) + .mapped("room_type_id") + .mapped("class_id") + ) + else: + room_type_classes = self.env["pms.room.type.class"].search( + [("pms_property_ids", "=", False)] + ) + result_room_type_class = [] + PmsRoomTypeClassInfo = self.env.datamodels["pms.room.type.class.info"] + for room in room_type_classes: + result_room_type_class.append( + PmsRoomTypeClassInfo( + id=room.id, + name=room.name, + defaultCode=room.default_code if room.default_code else None, + pmsPropertyIds=room.pms_property_ids.mapped("id"), + imageUrl=url_image_pms_api_rest( + "pms.room.type.class", room.id, "icon_pms_api_rest" + ), + ) + ) + return result_room_type_class diff --git a/pms_api_rest/services/pms_room_type_service.py b/pms_api_rest/services/pms_room_type_service.py new file mode 100644 index 0000000000..1ab9084ece --- /dev/null +++ b/pms_api_rest/services/pms_room_type_service.py @@ -0,0 +1,102 @@ +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.exceptions import MissingError + + +class PmsRoomTypeService(Component): + _inherit = "base.rest.service" + _name = "pms.room.type.service" + _usage = "room-types" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.room.type.search.param"), + output_param=Datamodel("pms.room.type.info", is_list=True), + auth="jwt_api_pms", + ) + def get_room_types(self, room_type_search_param): + room_type_all_properties = self.env["pms.room.type"].search( + [("pms_property_ids", "=", False)] + ) + if room_type_search_param.pmsPropertyIds: + room_types = set() + for index, prop in enumerate(room_type_search_param.pmsPropertyIds): + room_types_with_query_property = self.env["pms.room.type"].search( + [("pms_property_ids", "=", prop)] + ) + if index == 0: + room_types = set(room_types_with_query_property.ids) + else: + room_types = room_types.intersection( + set(room_types_with_query_property.ids) + ) + room_types_total = list( + set(list(room_types) + room_type_all_properties.ids) + ) + else: + room_types_total = list(room_type_all_properties.ids) + domain = [ + ("id", "in", room_types_total), + ] + + result_rooms = [] + PmsRoomTypeInfo = self.env.datamodels["pms.room.type.info"] + for room in self.env["pms.room.type"].search( + domain, + ): + + result_rooms.append( + PmsRoomTypeInfo( + id=room.id, + name=room.name, + pmsPropertyIds=room.pms_property_ids.mapped("id"), + defaultCode=room.default_code, + price=round(room.list_price, 2), + minPrice=room.min_price, + classId=room.class_id, + defaultMaxAvail=room.default_max_avail, + defaultQuota=room.default_quota, + ) + ) + return result_rooms + + @restapi.method( + [ + ( + [ + "/restricted/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.room.type.info", is_list=False), + auth="jwt_api_pms", + ) + def get_restricted_room_type(self, room_type_id): + room_type_record = self.env["pms.room.type"].sudo().browse( + room_type_id + ) + if room_type_record.exists(): + PmsRoomTypeInfo = self.env.datamodels["pms.room.type.info"] + return PmsRoomTypeInfo( + id=room_type_record.id, + name=room_type_record.name, + pmsPropertyIds=room_type_record.pms_property_ids.mapped("id"), + defaultCode=room_type_record.default_code, + price=round(room_type_record.list_price, 2), + minPrice=room_type_record.min_price, + classId=room_type_record.class_id, + defaultMaxAvail=room_type_record.default_max_avail, + defaultQuota=room_type_record.default_quota, + ) + else: + raise MissingError("Room Type Class not found") diff --git a/pms_api_rest/services/pms_sale_channel_service.py b/pms_api_rest/services/pms_sale_channel_service.py new file mode 100644 index 0000000000..d336874ec0 --- /dev/null +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -0,0 +1,100 @@ +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +from ..pms_api_rest_utils import url_image_pms_api_rest + + +class PmsSaleChannelService(Component): + _inherit = "base.rest.service" + _name = "pms.sale.channel.service" + _usage = "sale-channels" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.sale.channel.search.param"), + output_param=Datamodel("pms.sale.channel.info", is_list=True), + auth="jwt_api_pms", + ) + def get_sale_channels(self, sale_channel_search_param): + sale_channels_all_properties = self.env["pms.sale.channel"].search( + [("pms_property_ids", "=", False)] + ) + if sale_channel_search_param.pmsPropertyIds: + sale_channels = set() + for index, prop in enumerate(sale_channel_search_param.pmsPropertyIds): + sale_channels_with_query_property = self.env["pms.sale.channel"].search( + [("pms_property_ids", "=", prop)] + ) + if index == 0: + sale_channels = set(sale_channels_with_query_property.ids) + else: + sale_channels = sale_channels.intersection( + set(sale_channels_with_query_property.ids) + ) + sale_channels_total = list( + set(list(sale_channels) + sale_channels_all_properties.ids) + ) + else: + sale_channels_total = list(sale_channels_all_properties.ids) + domain = [ + ("id", "in", sale_channels_total), + ] + if sale_channel_search_param.isOnLine: + domain.append(("is_on_line", "=", sale_channel_search_param.isOnLine)) + + result_sale_channels = [] + PmsSaleChannelInfo = self.env.datamodels["pms.sale.channel.info"] + for sale_channel in self.env["pms.sale.channel"].search( + domain, + ): + result_sale_channels.append( + PmsSaleChannelInfo( + id=sale_channel.id, + name=sale_channel.name if sale_channel.name else None, + channelType=sale_channel.channel_type + if sale_channel.channel_type + else None, + iconUrl=url_image_pms_api_rest( + "pms.sale.channel", sale_channel.id, "icon" + ), + isOnLine=sale_channel.is_on_line, + ) + ) + return result_sale_channels + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.sale.channel.info", is_list=False), + auth="jwt_api_pms", + ) + def get_sale_channel(self, sale_channel_id): + sale_channel = self.env["pms.sale.channel"].search( + [("id", "=", sale_channel_id)] + ) + if sale_channel: + PmsSaleChannelInfo = self.env.datamodels["pms.sale.channel.info"] + return PmsSaleChannelInfo( + id=sale_channel.id, + name=sale_channel.name if sale_channel else None, + ) + else: + raise MissingError(_("Sale Channel not found")) diff --git a/pms_api_rest/services/pms_service_line_service.py b/pms_api_rest/services/pms_service_line_service.py new file mode 100644 index 0000000000..0cb90cc251 --- /dev/null +++ b/pms_api_rest/services/pms_service_line_service.py @@ -0,0 +1,96 @@ +from datetime import datetime + +from odoo import _ +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsServiceLineService(Component): + _inherit = "base.rest.service" + _name = "pms.service.line.service" + _usage = "service-lines" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.service.line.info", is_list=False), + auth="jwt_api_pms", + ) + def get_service_line(self, service_line_id): + service_line = self.env["pms.service.line"].search( + [("id", "=", service_line_id)] + ) + if not service_line: + raise MissingError(_("Service line not found")) + PmsServiceLineInfo = self.env.datamodels["pms.service.line.info"] + + return PmsServiceLineInfo( + id=service_line.id, + date=datetime.combine(service_line.date, datetime.min.time()).isoformat(), + priceUnit=round(service_line.price_unit, 2), + discount=round(service_line.discount, 2), + quantity=service_line.day_qty, + ) + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.service.line.info"), + auth="jwt_api_pms", + ) + def update_service_line(self, service_line_id, pms_service_line_info_data): + service_line = self.env["pms.service.line"].search( + [("id", "=", service_line_id)] + ) + vals = {} + if service_line: + if pms_service_line_info_data.date: + vals["date"] = datetime.strptime( + pms_service_line_info_data.date, "%Y-%m-%d" + ).date() + if pms_service_line_info_data.discount is not None: + vals["discount"] = pms_service_line_info_data.discount + if pms_service_line_info_data.quantity is not None: + vals["day_qty"] = pms_service_line_info_data.quantity + if pms_service_line_info_data.priceUnit is not None: + vals["price_unit"] = pms_service_line_info_data.priceUnit + service_line.write(vals) + else: + raise MissingError(_("Service line not found")) + + @restapi.method( + [ + ( + [ + "/", + ], + "DELETE", + ) + ], + auth="jwt_api_pms", + ) + def delete_service_line(self, service_line_id): + # esto tb podría ser con un browse + service_line = self.env["pms.service.line"].search( + [("id", "=", service_line_id)] + ) + if service_line: + service_line.unlink() + else: + raise MissingError(_("Service line not found")) diff --git a/pms_api_rest/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py new file mode 100644 index 0000000000..4d367cc915 --- /dev/null +++ b/pms_api_rest/services/pms_service_service.py @@ -0,0 +1,209 @@ +import logging +from datetime import datetime + +from odoo import _, fields +from odoo.exceptions import MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +class PmsServiceService(Component): + _inherit = "base.rest.service" + _name = "pms.service.service" + _usage = "services" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.service.info", is_list=False), + auth="jwt_api_pms", + ) + def get_service(self, service_id): + service = self.env["pms.service"].search([("id", "=", service_id)]) + if not service: + raise MissingError(_("Service not found")) + PmsServiceInfo = self.env.datamodels["pms.service.info"] + lines = [ + self.env.datamodels["pms.service.line.info"]( + id=line.id, + date=datetime.combine(line.date, datetime.min.time()).isoformat(), + priceUnit=line.price_unit, + discount=line.discount, + quantity=line.day_qty, + ) + for line in service.service_line_ids + ] + return PmsServiceInfo( + id=service.id, + name=service.name, + productId=service.product_id.id, + quantity=service.product_qty, + priceTotal=round(service.price_total, 2), + priceSubtotal=round(service.price_subtotal, 2), + priceTaxes=round(service.price_tax, 2), + discount=round(service.discount, 2), + isBoardService=service.is_board_service, + serviceLines=lines, + ) + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.service.info", is_list=False), + auth="jwt_api_pms", + ) + def update_service(self, service_id, service_data): + service = self.env["pms.service"].search([("id", "=", service_id)]) + if not service: + raise MissingError(_("Service not found")) + vals = {} + if service_data.serviceLines: + cmds_lines = [] + date_list = [] + for line_data in service_data.serviceLines: + date_line = datetime.strptime(line_data.date, "%Y-%m-%d").date() + date_list.append(date_line) + service_line = service.service_line_ids.filtered( + lambda l: l.date == date_line + ) + # 1- update values in existing lines + if service_line: + line_vals = self._get_service_lines_mapped(line_data, service_line) + cmds_lines.append((1, service_line.id, line_vals)) + # 2- create new lines + else: + line_vals = self._get_service_lines_mapped(line_data) + line_vals["date"] = line_data.date + cmds_lines.append((0, False, line_vals)) + # 3- delete old lines: + for line in service.service_line_ids.filtered( + lambda l: l.date not in date_list + ): + cmds_lines.append((2, line.id)) + if cmds_lines: + vals["service_line_ids"] = cmds_lines + _logger.info(vals) + if vals: + service.write(vals) + + def _get_service_lines_mapped(self, origin_data, service_line=False): + # Return dict witch reservation.lines values (only modified if line exist, + # or all pass values if line not exist) + line_vals = {} + if origin_data.priceUnit is not None and ( + not service_line or origin_data.priceUnit != service_line.price_unit + ): + line_vals["price_unit"] = origin_data.priceUnit + if origin_data.discount is not None and ( + not service_line or origin_data.discount != service_line.discount + ): + line_vals["discount"] = origin_data.discount + if origin_data.quantity is not None and ( + not service_line or origin_data.quantity != service_line.day_qty + ): + line_vals["day_qty"] = origin_data.quantity + return line_vals + + @restapi.method( + [ + ( + [ + "/", + ], + "DELETE", + ) + ], + auth="jwt_api_pms", + ) + def delete_service(self, service_id): + # esto tb podría ser con un browse + service = self.env["pms.service"].search([("id", "=", service_id)]) + if service: + service.unlink() + else: + raise MissingError(_("Service not found")) + + @restapi.method( + [ + ( + [ + "//service-lines", + ], + "GET", + ) + ], + output_param=Datamodel("pms.service.line.info", is_list=True), + auth="jwt_api_pms", + ) + def get_service_lines(self, service_id): + service = self.env["pms.service"].search([("id", "=", service_id)]) + if not service: + raise MissingError(_("Service not found")) + result_service_lines = [] + PmsServiceLineInfo = self.env.datamodels["pms.service.line.info"] + for service_line in service.service_line_ids: + result_service_lines.append( + PmsServiceLineInfo( + id=service_line.id, + date=datetime.combine( + service_line.date, datetime.min.time() + ).isoformat(), + priceUnit=round(service_line.price_unit, 2), + discount=round(service_line.discount, 2), + quantity=service_line.day_qty, + ) + ) + return result_service_lines + + @restapi.method( + [ + ( + [ + "/services-report", + ], + "GET", + ) + ], + input_param=Datamodel("pms.report.search.param", is_list=False), + output_param=Datamodel("pms.report", is_list=False), + auth="jwt_api_pms", + ) + def services_report(self, pms_report_search_param): + pms_property_id = pms_report_search_param.pmsPropertyId + date_from = fields.Date.from_string(pms_report_search_param.dateFrom) + date_to = fields.Date.from_string(pms_report_search_param.dateTo) + query = self.env.ref("pms_api_rest.sql_export_services") + if not query: + raise MissingError(_("SQL query not found")) + report_wizard = self.env["sql.file.wizard"].create({"sql_export_id": query.id}) + report_wizard.x_date_from = date_from + report_wizard.x_date_to = date_to + report_wizard.x_pms_property_id = pms_property_id + if not report_wizard._fields.get( + "x_date_from" + ) or not report_wizard._fields.get("x_pms_property_id"): + raise MissingError( + _("The Query params was modifieds, please contact the administrator") + ) + report_wizard.export_sql() + file_name = report_wizard.file_name + base64EncodedStr = report_wizard.binary_file + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(fileName=file_name, binary=base64EncodedStr) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py new file mode 100644 index 0000000000..361aceb12c --- /dev/null +++ b/pms_api_rest/services/pms_transaction_service.py @@ -0,0 +1,731 @@ +import logging +from datetime import datetime + +import pytz +import werkzeug.exceptions + +from odoo import _, fields +from odoo.exceptions import UserError, ValidationError +from odoo.osv import expression +from odoo.tools import get_lang + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +class PmsTransactionService(Component): + _inherit = "base.rest.service" + _name = "pms.transaction.service" + _usage = "transactions" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.transaction.search.param", is_list=False), + output_param=Datamodel("pms.transaction.results", is_list=False), + auth="jwt_api_pms", + ) + def get_transactions(self, pms_transactions_search_param): + order_by_param = False + result_transactions = [] + domain_fields = [ + ("state", "=", "posted"), + ] + + if pms_transactions_search_param.transactionMethodId: + domain_fields.append( + ("journal_id", "=", pms_transactions_search_param.transactionMethodId), + ) + elif pms_transactions_search_param.pmsPropertyId: + pms_property = self.env["pms.property"].browse( + pms_transactions_search_param.pmsPropertyId + ) + available_journals = pms_property._get_payment_methods( + automatic_included=True + ) + # REVIEW: avoid send to app generic company journals + available_journals = available_journals.filtered( + lambda j: j.pms_property_ids + ) + domain_fields.append(("journal_id", "in", available_journals.ids)) + domain_filter = list() + if pms_transactions_search_param.filter: + # TODO: filter by folio and invoice + for search in pms_transactions_search_param.filter.split(" "): + subdomains = [ + [("name", "ilike", search)], + [("ref", "ilike", search)], + [("partner_id.display_name", "ilike", search)], + ] + domain_filter.append(expression.OR(subdomains)) + + if ( + pms_transactions_search_param.dateStart + and pms_transactions_search_param.dateEnd + ): + date_from = fields.Date.from_string(pms_transactions_search_param.dateStart) + date_to = fields.Date.from_string(pms_transactions_search_param.dateEnd) + domain_fields.extend( + [ + ("date", ">=", date_from), + ("date", "<=", date_to), + ] + ) + + if pms_transactions_search_param.transactionType: + transaction_types = pms_transactions_search_param.transactionType.split(",") + type_domain = [] + + for transaction_type in transaction_types: + payment_type, partner_type = self._get_mapper_transaction_type( + transaction_type + ) + type_domain.append( + [ + ["partner_type", "=", partner_type], + ["payment_type", "=", payment_type], + ] + ) + + if type_domain: + type_domain = expression.OR(type_domain) + domain_fields.extend(type_domain) + if domain_filter: + domain = expression.AND([domain_fields, domain_filter[0]]) + else: + domain = domain_fields + PmsTransactionResults = self.env.datamodels["pms.transaction.results"] + PmsTransactiontInfo = self.env.datamodels["pms.transaction.info"] + total_transactions = self.env["account.payment"].search_count(domain) + group_transactions = self.env["account.payment"].read_group( + domain=domain, fields=["amount:sum"], groupby=["payment_type"] + ) + amount_result = 0 + if group_transactions: + total_inbound = next( + ( + item["amount"] + for item in group_transactions + if item["payment_type"] == "inbound" + ), + 0, + ) + total_outbound = next( + ( + item["amount"] + for item in group_transactions + if item["payment_type"] == "outbound" + ), + 0, + ) + amount_result = total_inbound - total_outbound + if pms_transactions_search_param.orderBy: + order_by_param = self._get_mapped_order_by_field( + pms_transactions_search_param.orderBy + ) + (" desc" if pms_transactions_search_param.orderDesc else " asc") + transactions = self.env["account.payment"].search( + domain, + order=order_by_param if order_by_param else False, + limit=pms_transactions_search_param.limit, + offset=pms_transactions_search_param.offset, + ) + for transaction in transactions: + # In internal transfer payments, the APP only show + # the outbound payment, with the countrapart journal id + # (destinationJournalId), the domain ensure avoid + # get the input internal transfer payment + destination_journal_id = False + if transaction.is_internal_transfer: + if ( + transaction.payment_type == "inbound" + and transaction.pms_api_counterpart_payment_id.id + in transactions.ids + ): + continue + outbound_transaction = ( + transaction + if transaction.payment_type == "outbound" + else transaction.pms_api_counterpart_payment_id + ) + inbound_transaction = ( + transaction + if transaction.payment_type == "inbound" + else transaction.pms_api_counterpart_payment_id + ) + transaction = ( + outbound_transaction + if outbound_transaction + else inbound_transaction + ) + if inbound_transaction: + destination_journal_id = inbound_transaction.journal_id.id + + result_transactions.append( + PmsTransactiontInfo( + id=transaction.id, + name=transaction.name if transaction.name else None, + amount=round(transaction.amount, 2), + journalId=transaction.journal_id.id + if transaction.journal_id + else None, + destinationJournalId=destination_journal_id or None, + date=datetime.combine( + transaction.date, datetime.min.time() + ).isoformat(), + partnerId=transaction.partner_id.id + if transaction.partner_id + else None, + partnerName=transaction.partner_id.name + if transaction.partner_id + else None, + reference=transaction.ref if transaction.ref else None, + createUid=transaction.create_uid + if transaction.create_uid + else None, + transactionType=transaction.pms_api_transaction_type or None, + isReconcilied=(transaction.reconciled_statements_count > 0), + downPaymentInvoiceId=transaction.reconciled_invoice_ids.filtered( + lambda inv: inv._is_downpayment() + ), + ) + ) + return PmsTransactionResults( + transactions=result_transactions, + total=round(amount_result, 2), + totalTransactions=total_transactions, + ) + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.transaction.info", is_list=False), + auth="jwt_api_pms", + ) + def get_transaction(self, transaction_id): + PmsTransactiontInfo = self.env.datamodels["pms.transaction.info"] + transaction = self.env["account.payment"].browse(transaction_id) + destination_journal_id = False + if transaction.is_internal_transfer: + destination_journal_id = ( + transaction.pms_api_counterpart_payment_id.journal_id.id + ) + return PmsTransactiontInfo( + id=transaction.id, + name=transaction.name if transaction.name else None, + amount=transaction.amount, + journalId=transaction.journal_id.id if transaction.journal_id else None, + destinationJournalId=destination_journal_id or None, + date=datetime.combine(transaction.date, datetime.min.time()).isoformat(), + partnerId=transaction.partner_id.id if transaction.partner_id else None, + partnerName=transaction.partner_id.name if transaction.partner_id else None, + reference=transaction.ref if transaction.ref else None, + createUid=transaction.create_uid.id if transaction.create_uid else None, + transactionType=transaction.pms_api_transaction_type or None, + isReconcilied=(transaction.reconciled_statements_count > 0), + downPaymentInvoiceId=transaction.reconciled_invoice_ids.filtered( + lambda inv: inv._is_downpayment() + ), + ) + + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.transaction.info", is_list=False), + auth="jwt_api_pms", + ) + def create_transaction(self, pms_transaction_info): + pay_date = fields.Date.from_string(pms_transaction_info.date) + payment_type, partner_type = self._get_mapper_transaction_type( + pms_transaction_info.transactionType + ) + journal = self.env["account.journal"].browse(pms_transaction_info.journalId) + is_internal_transfer = ( + pms_transaction_info.transactionType == "internal_transfer" + ) + partner_id = ( + pms_transaction_info.partnerId + if pms_transaction_info.transactionType != "internal_transfer" + else journal.company_id.partner_id.id + ) + vals = { + "amount": pms_transaction_info.amount, + "journal_id": pms_transaction_info.journalId, + "date": pay_date, + "partner_id": partner_id, + "ref": pms_transaction_info.reference, + "state": "draft", + "payment_type": payment_type, + "partner_type": partner_type, + "is_internal_transfer": is_internal_transfer, + } + if is_internal_transfer: + vals["partner_bank_id"] = ( + self.env["account.journal"] + .browse(pms_transaction_info.destinationJournalId) + .bank_account_id.id + ) + pay = self.env["account.payment"].create(vals) + if journal.type == "cash": + # REVIEW: Temporaly, if not cash session open, create a new one automatically + # Review this in pms_folio_service (/charge & /refund) + # and in pms_transaction_service (POST) + last_session = self._get_last_cash_session(journal_id=journal.id) + if last_session.state != "open": + self._action_open_cash_session( + pms_property_id=journal.pms_property_ids[0].id + if journal.pms_property_ids + else False, + amount=last_session.balance_end_real, + journal_id=journal.id, + force=False, + ) + pay.sudo().action_post() + if is_internal_transfer: + if journal.type == "cash": + # REVIEW: Temporaly, if not cash session open, create a new one automatically + # Review this in pms_folio_service (/charge & /refund) + # and in pms_transaction_service (POST) + last_session = self._get_last_cash_session(journal_id=journal.id) + if last_session.state != "open": + self._action_open_cash_session( + pms_property_id=journal.pms_property_ids[0] + if journal.pms_property_ids + else False, + amount=last_session.balance_end_real, + journal_id=pms_transaction_info.destinationJournalId, + force=False, + ) + counterpart_vals = { + "amount": pms_transaction_info.amount, + "journal_id": pms_transaction_info.destinationJournalId, + "date": pay_date, + "partner_id": partner_id, + "ref": pms_transaction_info.reference, + "state": "draft", + "payment_type": "inbound", + "partner_type": partner_type, + "is_internal_transfer": is_internal_transfer, + } + countrepart_pay = self.env["account.payment"].create(counterpart_vals) + countrepart_pay.sudo().action_post() + pay.pms_api_counterpart_payment_id = countrepart_pay.id + countrepart_pay.pms_api_counterpart_payment_id = pay.id + return pay.id + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.transaction.info", is_list=False), + auth="jwt_api_pms", + ) + def update_transaction(self, transaction_id, pms_transaction_info): + transaction = self.env["account.payment"].browse(transaction_id) + vals = {} + transacion_type = pms_transaction_info.transactionType + counterpart_transaction = False + # TODO: Downpayment invoiced (search invoice, reverse it and create a new one) + # Get generic update vals + if pms_transaction_info.amount is not None and round( + pms_transaction_info.amount, 2 + ) != round(transaction.amount, 2): + vals["amount"] = pms_transaction_info.amount + # Review: review all input parameters in all services + # to determine the handling of values: False or 0, None, and empty string '' + if ( + pms_transaction_info.partnerId + and pms_transaction_info.partnerId != transaction.partner_id.id + ): + vals["partner_id"] = pms_transaction_info.partnerId + if ( + pms_transaction_info.reference is not None + and pms_transaction_info.reference != transaction.ref + ): + vals["ref"] = pms_transaction_info.reference + if pms_transaction_info.date and pms_transaction_info.date != transaction.date: + vals["date"] = fields.Date.from_string(pms_transaction_info.date) + if transacion_type == "internal_transfer": + counterpart_transaction = transaction.pms_api_counterpart_payment_id + if ( + pms_transaction_info.journalId + and pms_transaction_info.journalId != transaction.journal_id.id + ): + new_journal = self.env["account.journal"].browse( + pms_transaction_info.journalId + ) + if new_journal.type == "cash": + last_cash_session = self._get_last_cash_session(new_journal.id) + new_date = vals.get("date", transaction.date) + if new_date < last_cash_session.create_date.date(): + raise UserError( + _( + "You cannot create a cash payment for a date " + "before the last cash session" + ) + ) + transaction.sudo().action_draft() + transaction.sudo().action_cancel() + vals["journal_id"] = new_journal.id + transaction = transaction.copy() + if counterpart_transaction: + if ( + pms_transaction_info.destinationJournalId + and pms_transaction_info.destinationJournalId + != counterpart_transaction.journal_id.id + ): + new_counterpart_journal = self.env["account.journal"].browse( + pms_transaction_info.destinationJournalId + ) + counterpart_vals = vals.copy() + if new_counterpart_journal.type == "cash": + last_cash_session = self._get_last_cash_session( + new_counterpart_journal.id + ) + new_date = vals.get("date", counterpart_transaction.date) + if new_date < last_cash_session.create_date.date(): + raise UserError( + _( + "You cannot create a cash payment for a date " + "before the last cash session" + ) + ) + counterpart_transaction.sudo().action_draft() + counterpart_transaction.sudo().action_cancel() + counterpart_vals["journal_id"] = new_counterpart_journal.id + counterpart_transaction = counterpart_transaction.copy() + vals["partner_bank_id"] = ( + self.env["account.journal"] + .browse(pms_transaction_info.destinationJournalId) + .bank_account_id.id + ) + vals["counterpart_payment_id"] = counterpart_transaction.id + counterpart_vals["counterpart_payment_id"] = transaction.id + if vals: + transaction.sudo().action_draft() + transaction.sudo().write(vals) + transaction.sudo().action_post() + if counterpart_transaction: + counterpart_transaction.sudo().write(vals) + counterpart_transaction.sudo().action_post() + return transaction.id + + @restapi.method( + [ + ( + [ + "/cash-register", + ], + "GET", + ) + ], + input_param=Datamodel("pms.cash.register.search.param", is_list=False), + output_param=Datamodel("pms.cash.register.info", is_list=False), + auth="jwt_api_pms", + ) + def get_cash_register(self, cash_register_search_param): + statement = self._get_last_cash_session( + journal_id=cash_register_search_param.journalId, + ) + CashRegister = self.env.datamodels["pms.cash.register.info"] + if not statement: + return CashRegister() + isOpen = True if statement.state == "open" else False + timezone = pytz.timezone(self.env.context.get("tz") or "UTC") + create_date_utc = pytz.UTC.localize(statement.create_date) + create_date = create_date_utc.astimezone(timezone) + date_done = False + if statement.date_done: + date_done_utc = pytz.UTC.localize(statement.date_done) + date_done = date_done_utc.astimezone(timezone) + + return CashRegister( + state="open" if isOpen else "close", + userId=statement.user_id.id, + balance=statement.balance_start if isOpen else statement.balance_end_real, + dateTime=create_date.isoformat() + if isOpen + else date_done.isoformat() + if date_done + else None, + ) + + @restapi.method( + [ + ( + [ + "/cash-register", + ], + "POST", + ) + ], + input_param=Datamodel("pms.cash.register.action", is_list=False), + output_param=Datamodel("pms.cash.register.result", is_list=False), + auth="jwt_api_pms", + ) + def cash_register(self, cash_register_action): + PmsCashRegisterResult = self.env.datamodels["pms.cash.register.result"] + if cash_register_action.action == "open": + dict_result = self._action_open_cash_session( + pms_property_id=cash_register_action.pmsPropertyId, + amount=cash_register_action.amount, + journal_id=cash_register_action.journalId, + force=cash_register_action.forceAction, + ) + elif cash_register_action.action == "close": + dict_result = self._action_close_cash_session( + pms_property_id=cash_register_action.pmsPropertyId, + amount=cash_register_action.amount, + journal_id=cash_register_action.journalId, + force=cash_register_action.forceAction, + ) + else: + raise ValidationError( + _("No action cash register found (only allowed open/close actions") + ) + return PmsCashRegisterResult( + result=dict_result["result"], + diff=dict_result["diff"], + ) + + def _action_open_cash_session(self, pms_property_id, amount, journal_id, force): + statement = self._get_last_cash_session( + journal_id=journal_id, + pms_property_id=pms_property_id, + ) + if round(statement.balance_end_real, 2) == round(amount, 2) or force: + self.env["account.bank.statement"].sudo().create( + { + "name": datetime.today().strftime(get_lang(self.env).date_format) + + " (" + + self.env.user.login + + ")", + "date": datetime.today(), + "balance_start": amount, + "journal_id": journal_id, + "pms_property_id": pms_property_id, + } + ) + diff = round(amount - statement.balance_end_real, 2) + return {"result": True, "diff": diff} + else: + diff = round(amount - statement.balance_end_real, 2) + return {"result": False, "diff": diff} + + def _action_close_cash_session(self, pms_property_id, amount, journal_id, force): + statement = self._get_last_cash_session( + journal_id=journal_id, + pms_property_id=pms_property_id, + ) + session_payments = ( + self.env["account.payment"] + .sudo() + .search( + [ + ("journal_id", "=", journal_id), + ("pms_property_id", "=", pms_property_id), + ("state", "=", "posted"), + ("create_date", ">=", statement.create_date), + ] + ) + ) + session_payments_amount = sum( + session_payments.filtered(lambda x: x.payment_type == "inbound").mapped( + "amount" + ) + ) - sum( + session_payments.filtered(lambda x: x.payment_type == "outbound").mapped( + "amount" + ) + ) + + compute_end_balance = round( + statement.balance_start + session_payments_amount, 2 + ) + if round(compute_end_balance, 2) == round(amount, 2): + self._session_create_statement_lines( + session_payments, statement, amount, auto_conciliation=True + ) + if statement.all_lines_reconciled: + statement.sudo().button_validate_or_action() + return { + "result": True, + "diff": 0, + } + elif force: + self._session_create_statement_lines( + session_payments, statement, amount, auto_conciliation=False + ) + diff = round(amount - compute_end_balance, 2) + return { + "result": True, + "diff": diff, + } + else: + diff = round(amount - compute_end_balance, 2) + return { + "result": False, + "diff": diff, + } + + @restapi.method( + [ + ( + [ + "/transactions-report", + ], + "GET", + ) + ], + input_param=Datamodel("pms.report.search.param", is_list=False), + output_param=Datamodel("pms.report", is_list=False), + auth="jwt_api_pms", + ) + def transactions_report(self, pms_transaction_report_search_param): + pms_property_id = pms_transaction_report_search_param.pmsPropertyId + date_from = fields.Date.from_string( + pms_transaction_report_search_param.dateFrom + ) + date_to = fields.Date.from_string(pms_transaction_report_search_param.dateTo) + + report_wizard = self.env["cash.daily.report.wizard"].create( + { + "date_start": date_from, + "date_end": date_to, + "pms_property_id": pms_property_id, + } + ) + result = report_wizard._export() + file_name = result["xls_filename"] + base64EncodedStr = result["xls_binary"] + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(fileName=file_name, binary=base64EncodedStr) + + def _get_mapper_transaction_type(self, transaction_type): + if transaction_type == "internal_transfer": + # counterpart is inbound supplier + return "outbound", "supplier" + elif transaction_type == "customer_inbound": + return "inbound", "customer" + elif transaction_type == "customer_outbound": + return "outbound", "customer" + elif transaction_type == "supplier_inbound": + return "inbound", "supplier" + elif transaction_type == "supplier_outbound": + return "outbound", "supplier" + + def _session_create_statement_lines( + self, session_payments, statement, amount, auto_conciliation + ): + payment_statement_line_match_dict = [] + for record in session_payments: + journal = record.journal_id + vals = { + "date": record.date, + "journal_id": journal.id, + "amount": record.amount + if record.payment_type == "inbound" + else -record.amount, + "payment_ref": record.ref, + "partner_id": record.partner_id.id, + "pms_property_id": record.pms_property_id.id, + "statement_id": statement.id, + } + statement_line = self.env["account.bank.statement.line"].sudo().create(vals) + payment_statement_line_match_dict.append( + { + "payment_id": record.id, + "statement_line_id": statement_line.id, + } + ) + + # Not call to button post to avoid create profit/loss line + # (_check_balance_end_real_same_as_computed) + if not statement.name: + statement.sudo()._set_next_sequence() + statement.sudo().balance_end_real = amount + statement.write({"state": "posted"}) + lines_of_moves_to_post = statement.line_ids.filtered( + lambda line: line.move_id.state != "posted" + ) + if lines_of_moves_to_post: + lines_of_moves_to_post.move_id._post(soft=False) + + if auto_conciliation: + for match in payment_statement_line_match_dict: + payment = self.env["account.payment"].sudo().browse(match["payment_id"]) + statement_line = ( + self.env["account.bank.statement.line"] + .sudo() + .browse(match["statement_line_id"]) + ) + payment_move_line = payment.move_id.line_ids.filtered( + lambda x: x.reconciled is False + and x.journal_id == journal + and ( + x.account_id == journal.payment_debit_account_id + or x.account_id == journal.payment_credit_account_id + ) + ) + statement_line_move = statement_line.move_id + statement_move_line = statement_line_move.line_ids.filtered( + lambda line: line.account_id.reconcile + or line.account_id == line.journal_id.suspense_account_id + ) + if payment_move_line and statement_move_line: + statement_move_line.account_id = payment_move_line.account_id + lines_to_reconcile = payment_move_line + statement_move_line + lines_to_reconcile.reconcile() + + def _get_last_cash_session(self, journal_id, pms_property_id=False): + domain = [("journal_id", "=", journal_id)] + if pms_property_id: + domain.append(("pms_property_id", "=", pms_property_id)) + return ( + self.env["account.bank.statement"] + .sudo() + .search( + domain, + order="date desc, id desc", + limit=1, + ) + ) + + def _get_mapped_order_by_field(self, field): + if field == "name": + result = "name" + elif field == "date": + result = "date" + elif field == "reference": + result = "ref" + elif field == "amount": + result = "amount" + else: + raise werkzeug.exceptions.MethodNotAllowed(description="Field not allowed") + return result diff --git a/pms_api_rest/services/pms_ubication_service.py b/pms_api_rest/services/pms_ubication_service.py new file mode 100644 index 0000000000..4e47d362ad --- /dev/null +++ b/pms_api_rest/services/pms_ubication_service.py @@ -0,0 +1,50 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsUbicationService(Component): + _inherit = "base.rest.service" + _name = "pms.ubication.service" + _usage = "ubications" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.ubication.search.param"), + output_param=Datamodel("pms.ubication.info", is_list=True), + auth="jwt_api_pms", + ) + def get_ubications(self, ubication_search_param): + if ubication_search_param.pmsPropertyIds: + ubications = ( + self.env["pms.room"] + .search( + [("pms_property_id", "in", ubication_search_param.pmsPropertyIds)] + ) + .mapped("ubication_id") + ) + else: + ubications = self.env["pms.ubication"].search( + [("pms_property_ids", "=", False)] + ) + + result_ubications = [] + PmsUbicationInfo = self.env.datamodels["pms.ubication.info"] + for ubication in ubications: + + result_ubications.append( + PmsUbicationInfo( + id=ubication.id, + name=ubication.name, + pmsPropertyIds=ubication.pms_property_ids.mapped("id"), + ) + ) + return result_ubications diff --git a/pms_api_rest/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py new file mode 100644 index 0000000000..0b70063a3c --- /dev/null +++ b/pms_api_rest/services/pms_user_service.py @@ -0,0 +1,205 @@ +import base64 +import os +import tempfile +from datetime import datetime, timedelta + +import werkzeug.exceptions + +from odoo import _ +from odoo.exceptions import AccessDenied, MissingError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +from ..pms_api_rest_utils import url_image_pms_api_rest + + +class PmsRoomTypeClassService(Component): + _inherit = "base.rest.service" + _name = "pms.user.service" + _usage = "users" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.api.rest.user.output", is_list=False), + auth="jwt_api_pms", + ) + def get_user(self, user_id): + user = self.env["res.users"].sudo().search([("id", "=", user_id)]) + if user: + PmsUserInfo = self.env.datamodels["pms.api.rest.user.output"] + return PmsUserInfo( + userId=user.id, + userName=user.name, + userFirstName=user.firstname if user.firstname else "", + userEmail=user.email if user.email else "", + userPhone=user.phone if user.phone else "", + userImageBase64=user.image_1920 if user.image_1920 else "", + userImageUrl=url_image_pms_api_rest( + "res.partner", user.partner_id.id, "image_1024" + ), + isNewInterfaceUser=user.is_new_interface_app_user, + ) + + else: + raise MissingError(_("Folio not found")) + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.api.rest.user.output", is_list=False), + auth="jwt_api_pms", + ) + def write_user(self, user_id, input_data): + user = self.env["res.users"].sudo().search([("id", "=", user_id)]) + if user: + if input_data.isNewInterfaceUser is not None: + user.write( + { + "is_new_interface_app_user": input_data.isNewInterfaceUser, + } + ) + user.write( + { + "name": input_data.userName, + "email": input_data.userEmail, + "phone": input_data.userPhone, + } + ) + if input_data.userImageBase64 is not None: + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(base64.b64decode(input_data.userImageBase64)) + temp_path = f.name + + with open(temp_path, "rb") as f: + user_image = f.read() + os.unlink(temp_path) + + user.write( + { + "image_1024": base64.b64encode(user_image), + } + ) + else: + user.write( + { + "image_1024": "", + } + ) + return True + + @restapi.method( + [ + ( + [ + "/p//change-password", + ], + "PATCH", + ) + ], + output_param=Datamodel("pms.api.rest.user.login.output", is_list=False), + input_param=Datamodel("pms.api.rest.user.input", is_list=False), + auth="jwt_api_pms", + ) + def change_password(self, user_id, input_data): + user = self.env["res.users"].sudo().search([("id", "=", user_id)]) + if user: + try: + user.with_user(user)._check_credentials(input_data.password, None) + except AccessDenied: + raise werkzeug.exceptions.Unauthorized(_("Wrong password")) + + user.change_password(input_data.password, input_data.newPassword) + + PmsUserInfo = self.env.datamodels["pms.api.rest.user.login.output"] + return PmsUserInfo( + login=user.login, + ) + + @restapi.method( + [ + ( + [ + "/p/reset-password", + ], + "POST", + ) + ], + input_param=Datamodel("pms.api.rest.user.input", is_list=False), + auth="public", + cors="*", + ) + def reset_password(self, input_data): + values = { + "password": input_data.password, + } + self.env["res.users"].sudo().signup(values, input_data.resetToken) + return True + + @restapi.method( + [ + ( + [ + "/send-mail-reset-password", + ], + "POST", + ) + ], + input_param=Datamodel("pms.api.rest.user.input", is_list=False), + auth="public", + cors="*", + ) + def send_mail_to_reset_password(self, input_data): + user = ( + self.env["res.users"].sudo().search([("email", "=", input_data.userEmail)]) + ) + if user: + template_id = self.env.ref("pms_api_rest.pms_reset_password_email").id + template = self.env["mail.template"].sudo().browse(template_id) + if not template: + return False + expiration_datetime = datetime.now() + timedelta(minutes=15) + user.partner_id.sudo().signup_prepare(expiration=expiration_datetime) + template.with_context({"app_url": input_data.url}).send_mail( + user.id, force_send=True + ) + return True + return False + + @restapi.method( + [ + ( + [ + "/check-reset-password-token/", + ], + "GET", + ) + ], + auth="public", + cors="*", + ) + def check_reset_password_token(self, reset_token): + user = ( + self.env["res.partner"].sudo().search([("signup_token", "=", reset_token)]) + ) + is_token_expired = False + if not user: + return True + if user.sudo().signup_expiration < datetime.now(): + is_token_expired = True + return is_token_expired diff --git a/pms_api_rest/services/res_city_zip_service.py b/pms_api_rest/services/res_city_zip_service.py new file mode 100644 index 0000000000..fc3b3fa933 --- /dev/null +++ b/pms_api_rest/services/res_city_zip_service.py @@ -0,0 +1,74 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class ResCityZipService(Component): + _inherit = "base.rest.service" + _name = "res.city.zip.service" + _usage = "zips" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("res.city.zip.search.param", is_list=False), + output_param=Datamodel("res.city.zip.info", is_list=True), + auth="public", + ) + def get_address_data(self, zip_search_param): + result_res_zip = [] + if not zip_search_param.address: + return result_res_zip + ResCityZipInfo = self.env.datamodels["res.city.zip.info"] + res_zip = ( + self.env["res.city.zip"] + .sudo() + .search([("display_name", "ilike", zip_search_param.address)], limit=10) + ) + + if res_zip: + for address in res_zip: + result_res_zip.append( + ResCityZipInfo( + resZipId=address.id, + cityId=address.city_id.name if address.city_id else None, + stateId=address.state_id.id if address.state_id else None, + stateName=address.state_id.name if address.state_id else None, + countryId=address.country_id.id if address.country_id else None, + zipCode=address.name if address.name else None, + ) + ) + return result_res_zip + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("res.city.zip.info", is_list=False), + auth="public", + ) + def get_address_data_by_zip(self, res_city_zip): + ResCityZipInfo = self.env.datamodels["res.city.zip.info"] + res_zip = self.env["res.city.zip"].sudo().search([("name", "=", res_city_zip)]) + if len(res_zip) > 1: + res_zip = res_zip[0] + if res_zip: + return ResCityZipInfo( + cityId=res_zip.city_id.name, + stateId=res_zip.state_id.id, + countryId=res_zip.country_id.id, + ) + else: + return ResCityZipInfo() diff --git a/pms_api_rest/services/res_country_service.py b/pms_api_rest/services/res_country_service.py new file mode 100644 index 0000000000..e073f42a4b --- /dev/null +++ b/pms_api_rest/services/res_country_service.py @@ -0,0 +1,68 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class ResCountryService(Component): + _inherit = "base.rest.service" + _name = "res.country.service" + _usage = "countries" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("res.country.info", is_list=True), + auth="public", + ) + def get_countries(self): + result_countries = [] + ResCountriesInfo = self.env.datamodels["res.country.info"] + for country in ( + self.env["res.country"] + .with_context(lang=self.env.user.lang) + .sudo() + .search([]) + ): + result_countries.append( + ResCountriesInfo( + id=country.id, + name=country.name, + code=country.code if country.code else None, + ) + ) + return result_countries + + @restapi.method( + [ + ( + [ + "//country-states", + ], + "GET", + ) + ], + output_param=Datamodel("res.country_state.info", is_list=True), + auth="public", + ) + def get_states(self, country_id): + result_country_states = [] + ResCountryStatesInfo = self.env.datamodels["res.country_state.info"] + for country_states in ( + self.env["res.country.state"] + .sudo() + .search([("country_id", "=", country_id)]) + ): + result_country_states.append( + ResCountryStatesInfo( + id=country_states.id, + name=country_states.name, + ) + ) + return result_country_states diff --git a/pms_api_rest/services/res_lang_service.py b/pms_api_rest/services/res_lang_service.py new file mode 100644 index 0000000000..c038974db6 --- /dev/null +++ b/pms_api_rest/services/res_lang_service.py @@ -0,0 +1,35 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class ReslANGService(Component): + _inherit = "base.rest.service" + _name = "res.lang.service" + _usage = "languages" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("res.lang.info", is_list=True), + auth="jwt_api_pms", + ) + def get_partners(self): + result_langs = [] + ResLangInfo = self.env.datamodels["res.lang.info"] + languages = self.env["res.lang"].get_installed() + for lang in languages: + result_langs.append( + ResLangInfo( + code=lang[0], + name=lang[1], + ) + ) + return result_langs diff --git a/pms_api_rest/services/res_partner_category_service.py b/pms_api_rest/services/res_partner_category_service.py new file mode 100644 index 0000000000..d63bfa00a0 --- /dev/null +++ b/pms_api_rest/services/res_partner_category_service.py @@ -0,0 +1,39 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsPartnerCategoriesService(Component): + _inherit = "base.rest.service" + _name = "res.partner.category.service" + _usage = "categories" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("res.partner.category.info", is_list=True), + auth="jwt_api_pms", + ) + def get_categories(self): + result_categories = [] + ResPartnerCategoryInfo = self.env.datamodels["res.partner.category.info"] + for category in ( + self.env["res.partner.category"] + .with_context(lang=self.env.user.lang) + .search([("is_used_in_checkin", "=", True)]) + ): + result_categories.append( + ResPartnerCategoryInfo( + id=category.id, + name=category.name, + parentId=category.parent_id.id if category.parent_id.id else 0, + ) + ) + return result_categories diff --git a/pms_api_rest/static/description/icon.png b/pms_api_rest/static/description/icon.png new file mode 100644 index 0000000000..bd2b188d63 Binary files /dev/null and b/pms_api_rest/static/description/icon.png differ diff --git a/pms_api_rest/views/pms_api_log_views.xml b/pms_api_rest/views/pms_api_log_views.xml new file mode 100644 index 0000000000..ec8b9bcbcf --- /dev/null +++ b/pms_api_rest/views/pms_api_log_views.xml @@ -0,0 +1,82 @@ + + + + pms.api.log.tree + pms.api.log + + + + + + + + + + + + + + + pms.api.log.form + pms.api.log + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + + pms.api.log.search + pms.api.log + + + + + + + + + + + + + API Logs + pms.api.log + tree,form + + + + +
diff --git a/pms_api_rest/views/pms_property_views.xml b/pms_api_rest/views/pms_property_views.xml new file mode 100644 index 0000000000..f419ad45ed --- /dev/null +++ b/pms_api_rest/views/pms_property_views.xml @@ -0,0 +1,124 @@ + + + + pms.property + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pms_api_rest/views/pms_room_type_class_views.xml b/pms_api_rest/views/pms_room_type_class_views.xml new file mode 100644 index 0000000000..c3fbae7f60 --- /dev/null +++ b/pms_api_rest/views/pms_room_type_class_views.xml @@ -0,0 +1,12 @@ + + + + pms.room.type.class + + + + + + + + diff --git a/pms_api_rest/views/product_template_views.xml b/pms_api_rest/views/product_template_views.xml new file mode 100644 index 0000000000..f509dc9a10 --- /dev/null +++ b/pms_api_rest/views/product_template_views.xml @@ -0,0 +1,14 @@ + + + + view.product.template.form.inherited + product.template + + + + + + + + + diff --git a/pms_api_rest/views/res_users_views.xml b/pms_api_rest/views/res_users_views.xml new file mode 100644 index 0000000000..39570f6b94 --- /dev/null +++ b/pms_api_rest/views/res_users_views.xml @@ -0,0 +1,56 @@ + + + + User Properties fields + res.users + + + + + + + + + + + + + + + + + + + diff --git a/pms_l10n_es/models/pms_checkin_partner.py b/pms_l10n_es/models/pms_checkin_partner.py index 02122cc61b..74264c24f4 100644 --- a/pms_l10n_es/models/pms_checkin_partner.py +++ b/pms_l10n_es/models/pms_checkin_partner.py @@ -90,7 +90,7 @@ def _checkin_mandatory_fields(self, residence_country=False, document_type=False "residence_state_id", ] ) - if document_type.code and document_type.code == CODE_NIF: + if document_type and document_type.code and document_type.code == CODE_NIF: mandatory_fields.extend( [ "lastname2", diff --git a/pms_ocr_klippa/README.rst b/pms_ocr_klippa/README.rst new file mode 100644 index 0000000000..aa976009d7 --- /dev/null +++ b/pms_ocr_klippa/README.rst @@ -0,0 +1,81 @@ +========== +OCR Klippa +========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2b9fc0252a9368c795df1d59126287bd5538d1a23498d99bfb72658e7a2a6eff + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpms-lightgray.png?logo=github + :target: https://github.com/OCA/pms/tree/14.0/pms_ocr_klippa + :alt: OCA/pms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pms-14-0/pms-14-0-pms_ocr_klippa + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/pms&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Module to connect the OCR Klippa with the pms + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Set api key klippa and url parameters of the OCR service and select klippa provider ocr in pms_property + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Commit [Sun] + +Contributors +~~~~~~~~~~~~ + +* Brais + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/pms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pms_ocr_klippa/__init__.py b/pms_ocr_klippa/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pms_ocr_klippa/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pms_ocr_klippa/__manifest__.py b/pms_ocr_klippa/__manifest__.py new file mode 100644 index 0000000000..96536750de --- /dev/null +++ b/pms_ocr_klippa/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2020-21 Jose Luis Algara (Alda Hotels ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "OCR Klippa", + "version": "14.0.1.0.1", + "author": "Commit [Sun], Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": True, + "category": "Generic Modules/Property Management System", + "website": "https://github.com/OCA/pms", + "depends": [ + "pms_api_rest", + ], + "external_dependencies": {"python": ["thefuzz", "geopy"]}, + "data": [ + "data/pms_ocr_klippa_data.xml", + "views/res_partner_id_category_views.xml", + "views/klippa_log_views.xml", + "security/ir.model.access.csv", + ], + "installable": True, +} diff --git a/pms_ocr_klippa/data/pms_ocr_klippa_data.xml b/pms_ocr_klippa/data/pms_ocr_klippa_data.xml new file mode 100644 index 0000000000..44afd83c9e --- /dev/null +++ b/pms_ocr_klippa/data/pms_ocr_klippa_data.xml @@ -0,0 +1,39 @@ + + + + + ocr_klippa_api_key + False + + + ocr_klippa_url + https://custom-ocr.klippa.com/api/v1/parseDocument/identity + + + + + P + P + + + I + I + + + I + D + + + I + R + + + diff --git a/pms_ocr_klippa/models/__init__.py b/pms_ocr_klippa/models/__init__.py new file mode 100644 index 0000000000..fc951df032 --- /dev/null +++ b/pms_ocr_klippa/models/__init__.py @@ -0,0 +1,3 @@ +from . import pms_property +from . import res_partner_id_category +from . import klippa_log diff --git a/pms_ocr_klippa/models/klippa_log.py b/pms_ocr_klippa/models/klippa_log.py new file mode 100644 index 0000000000..d379ddf5b6 --- /dev/null +++ b/pms_ocr_klippa/models/klippa_log.py @@ -0,0 +1,108 @@ +from datetime import timedelta + +from odoo import fields, models + + +class KlippaLog(models.Model): + _name = "klippa.log" + _order = "id desc" + + pms_property_id = fields.Many2one( + string="PMS Property", + help="PMS Property", + comodel_name="pms.property", + required=True, + ) + request_id = fields.Text( + string="Klippa Request ID", + help="Request Klippa ID", + ) + image_base64_front = fields.Text( + string="Front Image", + help="Front Image", + ) + image_base64_back = fields.Text( + string="Back Image", + help="Back Image", + ) + klippa_response = fields.Text( + string="Klippa Response", + help="Response", + ) + klippa_status = fields.Char( + string="Klippa Status", + help="Klippa Status", + ) + request_datetime = fields.Datetime( + string="Request Date", + help="Request Date", + ) + response_datetime = fields.Datetime( + string="Response Date", + help="Response Date", + ) + request_duration = fields.Float( + string="Request Duration", + help="Request Duration", + ) + mapped_duration = fields.Float( + string="Mapped Duration", + help="Mapped Duration", + ) + total_duration = fields.Float( + string="Total Duration", + help="Total Duration", + ) + endpoint = fields.Char( + string="Endpoint", + help="Endpoint", + ) + request_size = fields.Integer( + string="Request Size", + help="Request Size", + ) + response_size = fields.Integer( + string="Response Size", + help="Response Size", + ) + request_headers = fields.Text( + string="Request Headers", + help="Request Headers", + ) + request_url = fields.Char( + string="Request URL", + help="Request URL", + ) + service_response = fields.Text( + string="Resvice Response", + help="Resvice Response", + ) + final_status = fields.Char( + string="Final Status", + help="Final Status", + ) + error = fields.Text( + string="Error", + help="Error", + ) + nominatim_status = fields.Char( + string="Nominatim Status", + help="Nominatim Status", + ) + nominatim_response = fields.Text( + string="Nominatim Response", + help="Nominatim Response", + ) + + 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. + + """ + self.sudo().search( + [ + ("final_status", "=", "success"), + ("create_date", "<", fields.Datetime.now() - timedelta(days=offset)), + ] + ).unlink() diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py new file mode 100644 index 0000000000..ece9394731 --- /dev/null +++ b/pms_ocr_klippa/models/pms_property.py @@ -0,0 +1,558 @@ +import logging +import traceback +from datetime import date, datetime + +import requests +from dateutil.relativedelta import relativedelta +from thefuzz import process + +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + +NOMINATIM_URL = "https://nominatim.openstreetmap.org/search" + +CHECKIN_FIELDS = { + "nationality": "partner_id.nationality_id.id", + "country_id": "partner_id.residence_country_id.id", + "firstname": "partner_id.firstname", + "lastname": "partner_id.lastname", + "lastname2": "partner_id.lastname2", + "gender": "partner_id.gender", + "birthdate": "partner_id.birthdate_date", + "document_type": "category_id.id", + "document_expedition_date": "valid_from", + "document_support_number": "support_number", + "document_number": "name", + "residence_street": "partner_id.residence_street", + "residence_city": "partner_id.residence_city", + "country_state": "partner_id.residence_state_id.id", + "document_country_id": "country_id.id", + "zip": "partner_id.zip", +} + + +class PmsProperty(models.Model): + _inherit = "pms.property" + + ocr_checkin_supplier = fields.Selection(selection_add=[("klippa", "Klippa")]) + + # flake8: noqa: C901 + def _klippa_document_process(self, image_base_64_front, image_base_64_back=False): + try: + ocr_klippa_url = ( + self.env["ir.config_parameter"].sudo().get_param("ocr_klippa_url") + ) + ocr_klippa_api_key = ( + self.env["ir.config_parameter"].sudo().get_param("ocr_klippa_api_key") + ) + document = [] + if image_base_64_front: + document.append(image_base_64_front) + if image_base_64_back: + document.append(image_base_64_back) + if not document: + raise ValidationError(_("No document image found")) + + headers = { + "X-Auth-Key": ocr_klippa_api_key, + "Content-Type": "application/json", + } + payload = { + "document": document, + } + request_size = (len(image_base_64_front) if image_base_64_front else 0) + ( + len(image_base_64_back) if image_base_64_back else 0 + ) + log_data = { + "pms_property_id": self.id, + "image_base64_front": image_base_64_front, + "image_base64_back": image_base_64_back, + "request_datetime": datetime.now(), + "endpoint": ocr_klippa_url, + "request_size": request_size, + "request_headers": str(headers), + } + + # Call Klippa OCR API + result = requests.post( + ocr_klippa_url, + headers=headers, + json=payload, + ) + json_data = result.json() + log_data.update( + { + "klippa_response": json_data, + "klippa_status": json_data.get("result", "error"), + "response_datetime": datetime.now(), + "response_size": len(str(json_data)), + "request_duration": ( + datetime.now() - log_data["request_datetime"] + ).seconds, + "request_id": json_data.get("request_id", False), + } + ) + if json_data.get("result") != "success": + raise ValidationError(_("Error calling Klippa OCR API")) + document_data = json_data["data"]["parsed"] + init_mapped_datetime = datetime.now() + + mapped_data = self._map_klippa_data(document_data) + + if mapped_data.get("nominatim_status"): + log_data.update( + { + "nominatim_status": mapped_data["nominatim_status"], + } + ) + mapped_data.pop("nominatim_status") + if mapped_data.get("nominatim_response"): + log_data.update( + { + "nominatim_response": mapped_data["nominatim_response"], + } + ) + mapped_data.pop("nominatim_response") + + log_data.update( + { + "service_response": mapped_data, + "mapped_duration": (datetime.now() - init_mapped_datetime).seconds, + "total_duration": ( + datetime.now() - log_data["request_datetime"] + ).seconds, + "final_status": "success", + } + ) + self.env["klippa.log"].sudo().create(log_data) + return mapped_data + except Exception: + log_data.update( + { + "error": traceback.format_exc(), + "final_status": "error", + "total_duration": ( + datetime.now() - log_data["request_datetime"] + ).seconds, + } + ) + self.env["klippa.log"].sudo().create(log_data) + _logger.error(traceback.format_exc()) + return {} + + def _map_klippa_data(self, document_data): + mapped_data = {} + key_document_number, key_personal_number = self._get_number_keys(document_data) + for key, dict_value in document_data.items(): + if dict_value and isinstance(dict_value, dict): + value = dict_value.get("value", False) + else: + continue + # Residence Address -------------------------------------------------- + if key == "address" and value: + mapped_data = self._complete_residence_address(value, mapped_data) + + # Document Data -------------------------------------------------- + elif key == "issuing_country" and value: + mapped_data["document_country_id"] = self._get_country_id(value) + elif key == "document_type" and value: + mapped_data["document_type"] = self._get_document_type( + klippa_type=value, + klippa_subtype=document_data.get("document_subtype").get("value") + if document_data.get("document_subtype") + else False, + country_id=self._get_country_id( + document_data.get("issuing_country").get("value") + if document_data.get("issuing_country") + else False + ), + ).id + elif key == "personal_number" and value: + mapped_data[key_personal_number] = value + elif key == "document_number" and value: + mapped_data[key_document_number] = value + elif key == "date_of_issue" and value: + mapped_data["document_expedition_date"] = datetime.strptime( + value, "%Y-%m-%dT%H:%M:%S" + ).date() + elif ( + key == "date_of_expiry" + and value + and not document_data.get("date_of_issue", False) + ): + mapped_data["document_expedition_date"] = self._calc_expedition_date( + document_class_code=self._get_document_type( + klippa_type=document_data.get("document_class_code", False), + klippa_subtype=document_data.get("document_subtype").get( + "value" + ) + if document_data.get("document_subtype") + else False, + country_id=self._get_country_id( + document_data.get("issuing_country").get("value") + if document_data.get("issuing_country") + else False + ), + ), + date_of_expiry=value, + age=False, + date_of_birth=document_data.get("date_of_birth", False), + ) + + # Personal Data -------------------------------------------------- + elif key == "gender" and value: + if value == "M": + mapped_data["gender"] = "male" + elif value == "F": + mapped_data["gender"] = "female" + else: + mapped_data["gender"] = "other" + elif key == "given_names" and value: + mapped_data["firstname"] = value + elif key == "surname" and value: + mapped_data["lastname"] = self._get_surnames( + origin_surname=value, + )[0] + mapped_data["lastname2"] = self._get_surnames( + origin_surname=value, + )[1] + elif key == "date_of_birth" and value: + mapped_data["birthdate"] = datetime.strptime( + value, "%Y-%m-%dT%H:%M:%S" + ).date() + elif key == "nationality" and value: + mapped_data["nationality"] = self._get_country_id(value) + + # If the document number exist and not get the complete checkin information + # recovery the lost data from the found document + if mapped_data.get("document_number") and not all( + [mapped_data.get(field, False) for field in CHECKIN_FIELDS] + ): + document = self.env["res.partner.id_number"].search( + [ + ("name", "=", mapped_data["document_number"]), + ], + limit=1, + ) + if document: + mapped_data = self._complete_mapped_from_partner(document, mapped_data) + + return mapped_data + + def _calc_expedition_date( + self, document_class_code, date_of_expiry, age, date_of_birth + ): + result = False + person_age = False + if age: + person_age = age + elif date_of_birth and date_of_birth.get("value") != "": + date_of_birth = datetime.strptime( + date_of_birth.get("value"), "%Y-%m-%dT%H:%M:%S" + ).date() + person_age = relativedelta(date.today(), date_of_birth).years + if date_of_expiry and date_of_expiry != "" and person_age: + date_of_expiry = datetime.strptime( + date_of_expiry, "%Y-%m-%dT%H:%M:%S" + ).date() + if person_age < 30: + result = date_of_expiry - relativedelta(years=5) + elif ( + person_age >= 30 + and document_class_code + and document_class_code.code == "P" + ): + result = date_of_expiry - relativedelta(years=10) + elif 30 <= person_age < 70: + result = date_of_expiry - relativedelta(years=10) + return result if result else False + + def _get_number_keys(self, document_data): + # Heuristic to identify the mapping of document_number and document_support_number + # with respect to the personal_number and document_number fields of klippa + # If the klippa document type is "I", and it is Spanish, then the personal_number + # we map it against document_number and document_number against document_support_number + # otherwise, the document_number we map against document_number and the personal_number + # against document_support_number + key_document_number = "document_number" + key_personal_number = "document_support_number" + if ( + document_data.get("document_type", False) + and document_data.get("document_type").get("value") == "I" + and document_data.get("issuing_country", False) + and document_data.get("issuing_country").get("value") == "ESP" + ): + key_document_number = "document_support_number" + key_personal_number = "document_number" + return (key_document_number, key_personal_number) + + def _get_document_type(self, klippa_type, klippa_subtype=False, country_id=False): + # If we hace the issuing country, and document type is configured in the system + # to be used with the country, we use the country to get the document type + # If have issuing country and not found document type, we search a document type + # without country + # If not have issuing country, we search the document only by klippa code + document_type = False + domain = [("klippa_code", "=", klippa_type)] + if country_id: + domain.append(("country_ids", "in", country_id)) + document_type = self.env["res.partner.id_category"].search(domain, limit=1) + if not document_type and country_id: + document_type = self.env["res.partner.id_category"].search( + [ + ("klippa_code", "=", klippa_type), + ("country_ids", "=", False), + ], + ) + elif not document_type: + document_type = self.env["res.partner.id_category"].search( + [ + ("klippa_code", "=", klippa_type), + ], + ) + if len(document_type) > 1 and klippa_subtype: + # Try find document type by klippa_subtype_code, if not found, get the first + document_subtype = document_type.filtered( + lambda dt: dt.klippa_subtype_code == klippa_subtype + ) + document_type = ( + document_subtype[0] if document_subtype else document_type[0] + ) + if not document_type: + document_type = self.env.ref("pms.document_type_identification_document") + return document_type[0] if document_type else False + + def _get_country_id(self, country_code): + if not country_code: + return False + return ( + self.env["res.country"] + .search([("code_alpha3", "=", country_code)], limit=1) + .id + ) + + def _get_surnames(self, origin_surname): + # If origin surname has two or more surnames + # Get the last word like lastname2 and the rest like lastname + surnames = origin_surname.split(" ") + if len(surnames) > 1: + return (" ".join(surnames[:-1]), surnames[-1]) + return (origin_surname, False) + + def _complete_residence_address(self, value, mapped_data): + """ + This method tries to complete the residence address with the given data, + first we use the thefuzz library looking for acceptable matches + in the province and/or country name. + Once these data are completed, if the residence address has not been completed + we try to use the geopy library to complete the address with the data + """ + street_name = False + if "street_name" in value: + mapped_data["residence_street"] = value["street_name"] + ( + " " + value["house_number"] if "house_number" in value else "" + ) + street_name = value["street_name"] + if "city" in value: + mapped_data["residence_city"] = value["city"] + if "province" in value: + country_record = self._get_country_id(value.get("country", False)) + domain = [] + if country_record: + domain.append(("country_id", "=", country_record)) + candidates = process.extractOne( + value["province"], + self.env["res.country.state"].search(domain).mapped("name"), + ) + if candidates[1] >= 90: + country_state = self.env["res.country.state"].search( + domain + [("name", "=", candidates[0])], limit=1 + ) + mapped_data["country_state"] = country_state.id + if not country_record and country_state: + mapped_data["country_id"] = country_state.country_id.id + else: + mapped_data["country_state"] = False + if "country" in value and not mapped_data.get("country_id", False): + country_record = self._get_country_id(value["country"]) + mapped_data["country_id"] = country_record + if "postcode" in value: + mapped_data["zip"] = value["postcode"] + zip_code = self.env["res.city.zip"].search( + [ + ("name", "=", value["postcode"]), + ] + ) + if zip_code: + mapped_data["residence_city"] = ( + zip_code.city_id.name + if not mapped_data.get("residence_city", False) + else mapped_data["residence_city"] + ) + mapped_data["country_state"] = ( + zip_code.city_id.state_id.id + if not mapped_data.get("country_state", False) + else mapped_data["country_state"] + ) + mapped_data["country_id"] = ( + zip_code.city_id.state_id.country_id.id + if not mapped_data.get("country_id", False) + else mapped_data["country_id"] + ) + + address_data_dict = { + "zip": mapped_data.get("zip") or None, + "country_id": mapped_data.get("country_id") or None, + "countryState": mapped_data.get("country_state") or None, + "residence_city": mapped_data.get("residence_city") or None, + "residence_street": mapped_data.get("residence_street") or None, + } + # If we have one ore more values in address_data_dict, but not all, + # we try to complete the address + if any(address_data_dict.values()) and not all(address_data_dict.values()): + params = { + "format": "json", + "addressdetails": 1, + "language": "en", + "timeout": 5, + "limit": 1, + } + if address_data_dict.get("zip"): + params["postalcode"] = address_data_dict["zip"] + if address_data_dict.get("country_id"): + params["country"] = ( + self.env["res.country"].browse(address_data_dict["country_id"]).name + ) + if address_data_dict.get("countryState"): + params["state"] = ( + self.env["res.country.state"] + .browse(address_data_dict["countryState"]) + .name + ) + if address_data_dict.get("residence_city"): + params["city"] = address_data_dict["residence_city"] + + # Try to complete the address with Nominatim API + try: + params = self._get_nominatim_address(params, street_name, mapped_data) + except Exception: + _logger.error(traceback.format_exc()) + mapped_data["nominatim_status"] = "error" + mapped_data["nominatim_response"] = str(traceback.format_exc()) + return mapped_data + + def _get_nominatim_address(self, params, street_name, mapped_data): + if street_name: + # Clean street name with main words + street_words = street_name.split(" ") + params["street"] = " ".join( + [word for word in street_words if len(word) > 2] + ) + + try: + # Make the request to Nominatim + location = requests.get(NOMINATIM_URL, params=params, timeout=10) + location.raise_for_status() # Ensure we get a successful response + + if ( + location.headers.get("Content-Type") == "application/json" + and location.text + ): + locations = location.json() + if locations: + mapped_data["nominatim_response"] = locations + mapped_data["nominatim_status"] = "success" + location = locations[0] + _logger.info(location) + + # Update mapped data with address details + if not mapped_data.get("zip", False): + mapped_data["zip"] = location.get("address", {}).get( + "postcode", False + ) + if mapped_data["zip"]: + zip_code = self.env["res.city.zip"].search( + [("name", "=", mapped_data["zip"])] + ) + if zip_code: + mapped_data["residence_city"] = zip_code.city_id.name + mapped_data[ + "country_state" + ] = zip_code.city_id.state_id.id + mapped_data[ + "country_id" + ] = zip_code.city_id.state_id.country_id.id + + if not mapped_data.get("country_id", False): + country_code = ( + location.get("address", {}).get("country_code", "").upper() + ) + if country_code: + country_record = self.env["res.country"].search( + [("code", "=", country_code)] + ) + if not country_record and location.get("address", {}).get( + "country", False + ): + country_match = process.extractOne( + location.get("address", {}).get("country", False), + self.env["res.country"] + .with_context(lang="en_US") + .search([]) + .mapped("name"), + ) + if country_match and country_match[1] >= 90: + country_record = ( + self.env["res.country"] + .with_context(lang="en_US") + .search([("name", "=", country_match[0])]) + ) + if country_record: + mapped_data["country_id"] = country_record.id + + if not mapped_data.get("country_state", False): + state_name = location.get("address", {}).get( + "province" + ) or location.get("address", {}).get("state") + if state_name: + country_state_record = process.extractOne( + state_name, + self.env["res.country.state"].search([]).mapped("name"), + ) + if country_state_record and country_state_record[1] >= 90: + country_state = self.env["res.country.state"].search( + [("name", "=", country_state_record[0])], limit=1 + ) + if country_state: + mapped_data["country_state"] = country_state.id + + if not mapped_data.get("residence_city", False): + mapped_data["residence_city"] = location.get("address", {}).get( + "city", False + ) + + if not mapped_data.get("residence_street", False): + mapped_data["residence_street"] = location.get( + "address", {} + ).get("road", False) + + except requests.exceptions.RequestException as e: + _logger.error("Error in request to Nominatim: %s", traceback.format_exc()) + except requests.exceptions.JSONDecodeError as e: + _logger.error("Error decoding JSON response: %s", traceback.format_exc()) + except Exception: + _logger.error("Internal error: %s", traceback.format_exc()) + + return mapped_data + + def _complete_mapped_from_partner(self, document, mapped_data): + for key, field in CHECKIN_FIELDS.items(): + if ( + not mapped_data.get(key, False) + and document.mapped(field) + and document.mapped(field)[0] + ): + mapped_data[key] = document.mapped(field)[0] + return mapped_data diff --git a/pms_ocr_klippa/models/res_partner_id_category.py b/pms_ocr_klippa/models/res_partner_id_category.py new file mode 100644 index 0000000000..ebcf7d021e --- /dev/null +++ b/pms_ocr_klippa/models/res_partner_id_category.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResPartnerIdCategory(models.Model): + _inherit = "res.partner.id_category" + + klippa_code = fields.Char( + string="Klippa Code", + ) + klippa_subtype_code = fields.Char( + string="Klippa Subtype Code", + ) diff --git a/pms_ocr_klippa/readme/CONTRIBUTORS.rst b/pms_ocr_klippa/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..2401152c03 --- /dev/null +++ b/pms_ocr_klippa/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Brais diff --git a/pms_ocr_klippa/readme/DESCRIPTION.rst b/pms_ocr_klippa/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..bd81b811d6 --- /dev/null +++ b/pms_ocr_klippa/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Module to connect the OCR Klippa with the pms diff --git a/pms_ocr_klippa/readme/USAGE.rst b/pms_ocr_klippa/readme/USAGE.rst new file mode 100644 index 0000000000..b556f74692 --- /dev/null +++ b/pms_ocr_klippa/readme/USAGE.rst @@ -0,0 +1 @@ +Set api key klippa and url parameters of the OCR service and select klippa provider ocr in pms_property diff --git a/pms_ocr_klippa/security/ir.model.access.csv b/pms_ocr_klippa/security/ir.model.access.csv new file mode 100644 index 0000000000..7f2838d973 --- /dev/null +++ b/pms_ocr_klippa/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +techinal_klippa_log_access,techinal_klippa_log_access,model_klippa_log,base.group_system,1,1,1,1 diff --git a/pms_ocr_klippa/static/description/index.html b/pms_ocr_klippa/static/description/index.html new file mode 100644 index 0000000000..7043ce79f7 --- /dev/null +++ b/pms_ocr_klippa/static/description/index.html @@ -0,0 +1,428 @@ + + + + + +OCR Klippa + + + +
+

OCR Klippa

+ + +

Beta License: AGPL-3 OCA/pms Translate me on Weblate Try me on Runboat

+

Module to connect the OCR Klippa with the pms

+

Table of contents

+ +
+

Usage

+

Set api key klippa and url parameters of the OCR service and select klippa provider ocr in pms_property

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Commit [Sun]
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/pms project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/pms_ocr_klippa/views/klippa_log_views.xml b/pms_ocr_klippa/views/klippa_log_views.xml new file mode 100644 index 0000000000..20a307f0a6 --- /dev/null +++ b/pms_ocr_klippa/views/klippa_log_views.xml @@ -0,0 +1,116 @@ + + + + klippa.log.tree + klippa.log + + + + + + + + + + + + + klippa.log.form + klippa.log + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + klippa.log.search + klippa.log + + + + + + + + + + + + klippa.log.pivot + klippa.log + + + + + + + + + + klippa.log.graph + klippa.log + + + + + + + + + + Klippa Log + klippa.log + tree,form,pivot,graph + + + + +
diff --git a/pms_ocr_klippa/views/res_partner_id_category_views.xml b/pms_ocr_klippa/views/res_partner_id_category_views.xml new file mode 100644 index 0000000000..99bb520682 --- /dev/null +++ b/pms_ocr_klippa/views/res_partner_id_category_views.xml @@ -0,0 +1,16 @@ + + + + res.partner.id_category + + + + + + + + + diff --git a/pms_ocr_regula/README.rst b/pms_ocr_regula/README.rst new file mode 100644 index 0000000000..f9fe1e2f95 --- /dev/null +++ b/pms_ocr_regula/README.rst @@ -0,0 +1,81 @@ +========== +OCR Regula +========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4db37aab9c7f834aaf48397c989242dd06463f3a2a4b652d4d7dc2def9584db4 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpms-lightgray.png?logo=github + :target: https://github.com/OCA/pms/tree/14.0/pms_ocr_regula + :alt: OCA/pms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pms-14-0/pms-14-0-pms_ocr_regula + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/pms&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Module to connect the OCR regula with the pms + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Set api key klippa and url parameters of the OCR service and select regula provider ocr in pms_property + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Commit [Sun] + +Contributors +~~~~~~~~~~~~ + +* Brais + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/pms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pms_ocr_regula/__init__.py b/pms_ocr_regula/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pms_ocr_regula/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pms_ocr_regula/__manifest__.py b/pms_ocr_regula/__manifest__.py new file mode 100644 index 0000000000..2bb0bbed0c --- /dev/null +++ b/pms_ocr_regula/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2020-21 Jose Luis Algara (Alda Hotels ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "OCR Regula", + "version": "14.0.1.0.1", + "author": "Commit [Sun], Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": True, + "category": "Generic Modules/Property Management System", + "website": "https://github.com/OCA/pms", + "depends": [ + "pms_api_rest", + ], + "external_dependencies": { + "python": ["regula.documentreader.webclient", "marshmallow"], + }, + "data": ["data/pms_ocr_regula_data.xml"], + "installable": True, +} diff --git a/pms_ocr_regula/data/pms_ocr_regula_data.xml b/pms_ocr_regula/data/pms_ocr_regula_data.xml new file mode 100644 index 0000000000..a6a94753bd --- /dev/null +++ b/pms_ocr_regula/data/pms_ocr_regula_data.xml @@ -0,0 +1,11 @@ + + + + api_key_regula + False + + + ocr_regula_url + False + + diff --git a/pms_ocr_regula/models/__init__.py b/pms_ocr_regula/models/__init__.py new file mode 100644 index 0000000000..9216f6ffd4 --- /dev/null +++ b/pms_ocr_regula/models/__init__.py @@ -0,0 +1 @@ +from . import pms_property diff --git a/pms_ocr_regula/models/pms_property.py b/pms_ocr_regula/models/pms_property.py new file mode 100644 index 0000000000..340d0d0e51 --- /dev/null +++ b/pms_ocr_regula/models/pms_property.py @@ -0,0 +1,335 @@ +from datetime import date, datetime + +from dateutil.relativedelta import relativedelta +from regula.documentreader.webclient import ( + DocumentReaderApi, + ProcessParams, + RecognitionRequest, + Result, + Scenario, + TextFieldType, +) + +from odoo import fields, models + + +class PmsProperty(models.Model): + _inherit = "pms.property" + + ocr_checkin_supplier = fields.Selection(selection_add=[("regula", "Regula")]) + + def _regula_document_process(self, image_base_64_front, image_base_64_back=False): + ocr_regula_url = ( + self.env["ir.config_parameter"].sudo().get_param("ocr_regula_url") + ) + with DocumentReaderApi(host=ocr_regula_url) as api: + params = ProcessParams( + scenario=Scenario.FULL_PROCESS, + result_type_output=[ + Result.TEXT, + Result.STATUS, + Result.VISUAL_TEXT, + Result.DOCUMENT_TYPE, + ], + ) + request = RecognitionRequest( + process_params=params, images=[image_base_64_front] + ) + response = api.process(request) + if response.text and response.text.field_list: + id_country_spain = ( + self.env["res.country"].search([("code", "=", "ES")]).id + ) + country_id = self._process_nationality( + response.text.get_field(TextFieldType.NATIONALITY), + response.text.get_field(TextFieldType.NATIONALITY_CODE), + response.text.get_field(TextFieldType.NATIONALITY_CODE_NUMERIC), + ) + firstname, lastname, lastname2 = self._process_name( + id_country_spain, + country_id, + response.text.get_field(TextFieldType.GIVEN_NAMES), + response.text.get_field(TextFieldType.FIRST_SURNAME), + response.text.get_field(TextFieldType.SECOND_SURNAME), + response.text.get_field(TextFieldType.SURNAME), + response.text.get_field(TextFieldType.SURNAME_AND_GIVEN_NAMES), + ) + pms_ocr_checkin_result = dict() + if country_id: + pms_ocr_checkin_result["nationality"] = country_id + if firstname: + pms_ocr_checkin_result["firstname"] = firstname + if lastname: + pms_ocr_checkin_result["lastname"] = lastname + if lastname2: + pms_ocr_checkin_result["lastname2"] = lastname2 + gender = response.text.get_field(TextFieldType.SEX) + if gender and gender.value != "": + pms_ocr_checkin_result["gender"] = ( + "male" + if gender.value == "M" + else "female" + if gender.value == "F" + else "other" + ) + date_of_birth = response.text.get_field(TextFieldType.DATE_OF_BIRTH) + if date_of_birth and date_of_birth.value != "": + pms_ocr_checkin_result["birthdate"] = ( + datetime.strptime( + date_of_birth.value.replace("-", "/"), "%Y/%m/%d" + ) + .date() + .isoformat() + ) + date_of_expiry = response.text.get_field(TextFieldType.DATE_OF_EXPIRY) + age = response.text.get_field(TextFieldType.AGE) + document_class_code = response.text.get_field( + TextFieldType.DOCUMENT_CLASS_CODE + ) + if ( + document_class_code + and document_class_code.value != "" + and document_class_code.value == "P" + ): + pms_ocr_checkin_result["documentType"] = ( + self.env["res.partner.id_category"] + .search([("code", "=", "P")]) + .id + ) + date_of_issue = response.text.get_field(TextFieldType.DATE_OF_ISSUE) + if country_id == id_country_spain and ( + not date_of_issue or date_of_issue.value == "" + ): + date_of_issue = self._calc_expedition_date( + document_class_code, + date_of_expiry, + age, + date_of_birth, + ) + pms_ocr_checkin_result["documentExpeditionDate"] = date_of_issue + elif date_of_issue and date_of_issue.value != "": + pms_ocr_checkin_result[ + "documentExpeditionDate" + ] = date_of_issue.value.replace("-", "/") + support_number, document_number = self._proccess_document_number( + id_country_spain, + country_id, + document_class_code, + response.text.get_field(TextFieldType.DOCUMENT_NUMBER), + response.text.get_field(TextFieldType.PERSONAL_NUMBER), + ) + if support_number: + pms_ocr_checkin_result["documentSupportNumber"] = support_number + if document_number: + pms_ocr_checkin_result["documentNumber"] = document_number + address_street, address_city, address_area = self._process_address( + id_country_spain, + country_id, + response.text.get_field(TextFieldType.ADDRESS_STREET), + response.text.get_field(TextFieldType.ADDRESS_CITY), + response.text.get_field(TextFieldType.ADDRESS_AREA), + response.text.get_field(TextFieldType.ADDRESS), + ) + if address_street: + pms_ocr_checkin_result["residenceStreet"] = address_street + if address_city: + pms_ocr_checkin_result["residenceCity"] = address_city + if address_area: + pms_ocr_checkin_result["countryState"] = address_area + return pms_ocr_checkin_result + + def _process_nationality( + self, nationality, nationality_code, nationality_code_numeric + ): + country_id = False + country = False + if nationality_code_numeric and nationality_code_numeric.value != "": + country = self.env["res.country"].search( + [("code_numeric", "=", nationality_code_numeric.value)] + ) + elif nationality_code and nationality_code.value != "": + country = self.env["res.country"].search( + [("code_alpha3", "=", nationality_code.value)] + ) + elif nationality and nationality.value != "": + country = self.env["res.country"].search([("name", "=", nationality.value)]) + + if country: + country_id = country.id + + return country_id + + def _process_address( + self, + id_country_spain, + country_id, + address_street, + address_city, + address_area, + address, + ): + res_address_street = False + res_address_city = False + res_address_area = False + state = False + if country_id == id_country_spain: + if address_street and address_street.value != "": + res_address_street = address_street.value + if address_city and address_city.value != "": + res_address_city = address_city.value + if address_area and address_area.value != "": + res_address_area = address_area.value + if ( + address + and address != "" + and not (all([address_street, address_city, address_area])) + ): + address = address.value.replace("^", " ") + address_list = address.split(" ") + if not res_address_area: + res_address_area = address_list[-1] + if not res_address_city: + res_address_city = address_list[-2] + if not res_address_street: + res_address_street = address.replace( + res_address_area, "", 1 + ).replace(res_address_city, "", 1) + if res_address_area: + state = self.env["res.country.state"].search( + [("name", "ilike", res_address_area)] + ) + if state and len(state) == 1: + state = state.id + else: + if address and address.value != "": + res_address_street = address.value.replace("^", " ") + return res_address_street, res_address_city, state + + def _process_name( + self, + id_country_spain, + country_id, + given_names, + first_surname, + second_surname, + surname, + surname_and_given_names, + ): + firstname = False + lastname = False + lastname2 = False + + if surname_and_given_names.value and surname_and_given_names.value != "": + surname_and_given_names = surname_and_given_names.value.replace("^", " ") + + if given_names and given_names.value != "": + firstname = given_names.value + + if first_surname and first_surname.value != "": + lastname = first_surname.value + + if second_surname and second_surname.value != "": + lastname2 = second_surname.value + + if country_id == id_country_spain and not ( + all([firstname, lastname, lastname2]) + ): + if surname and surname.value != "": + lastname = lastname if lastname else surname.value.split(" ")[0] + lastname2 = lastname2 if lastname2 else surname.value.split(" ")[1:][0] + if ( + surname_and_given_names + and surname_and_given_names != "" + and not firstname + ): + firstname = surname_and_given_names.replace( + lastname, "", 1 + ).replace(lastname2, "", 1) + elif surname_and_given_names and surname_and_given_names != "": + lastname = ( + lastname if lastname else surname_and_given_names.split(" ")[0] + ) + lastname2 = ( + lastname2 if lastname2 else surname_and_given_names.split(" ")[1] + ) + firstname = ( + firstname + if firstname + else surname_and_given_names.replace(lastname, "", 1).replace( + lastname2, "", 1 + ) + ) + elif ( + country_id + and country_id != id_country_spain + and not (all([firstname, lastname])) + ): + if surname and surname.value != "": + lastname = lastname if lastname else surname.value + if ( + surname_and_given_names + and surname_and_given_names != "" + and not firstname + ): + firstname = surname_and_given_names.replace(lastname, "", 1) + elif surname_and_given_names and surname_and_given_names != "": + lastname = ( + lastname if lastname else surname_and_given_names.split(" ")[0] + ) + firstname = ( + firstname + if firstname + else surname_and_given_names.replace(lastname, "", 1) + ) + return firstname, lastname, lastname2 + + def _calc_expedition_date( + self, document_class_code, date_of_expiry, age, date_of_birth + ): + result = False + person_age = False + if age and age.value != "": + person_age = int(age.value) + elif date_of_birth and date_of_birth.value != "": + date_of_birth = datetime.strptime( + date_of_birth.value.replace("-", "/"), "%Y/%m/%d" + ).date() + person_age = relativedelta(date.today(), date_of_birth).years + if date_of_expiry and date_of_expiry.value != "" and person_age: + date_of_expiry = datetime.strptime( + date_of_expiry.value.replace("-", "/"), "%Y/%m/%d" + ).date() + if person_age < 30: + result = date_of_expiry - relativedelta(years=5) + elif ( + person_age >= 30 + and document_class_code + and document_class_code.value == "P" + ): + result = date_of_expiry - relativedelta(years=10) + elif 30 <= person_age < 70: + result = date_of_expiry - relativedelta(years=10) + return result.isoformat() if result else False + + def _proccess_document_number( + self, + id_country_spain, + country_id, + document_class_code, + document_number, + personal_number, + ): + res_support_number = False + res_document_number = False + if personal_number and personal_number.value != "": + res_document_number = personal_number.value + if document_number and document_number.value != "": + res_support_number = document_number.value + if ( + country_id == id_country_spain + and document_class_code + and document_class_code.value != "P" + ): + return res_support_number, res_document_number + else: + return False, res_support_number diff --git a/pms_ocr_regula/readme/CONTRIBUTORS.rst b/pms_ocr_regula/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..2401152c03 --- /dev/null +++ b/pms_ocr_regula/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Brais diff --git a/pms_ocr_regula/readme/DESCRIPTION.rst b/pms_ocr_regula/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..70adf77ab5 --- /dev/null +++ b/pms_ocr_regula/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Module to connect the OCR regula with the pms diff --git a/pms_ocr_regula/readme/USAGE.rst b/pms_ocr_regula/readme/USAGE.rst new file mode 100644 index 0000000000..4da449a3cd --- /dev/null +++ b/pms_ocr_regula/readme/USAGE.rst @@ -0,0 +1 @@ +Set api key klippa and url parameters of the OCR service and select regula provider ocr in pms_property diff --git a/pms_ocr_regula/static/description/index.html b/pms_ocr_regula/static/description/index.html new file mode 100644 index 0000000000..98ac023e20 --- /dev/null +++ b/pms_ocr_regula/static/description/index.html @@ -0,0 +1,426 @@ + + + + + + +OCR Regula + + + +
+

OCR Regula

+ + +

Beta License: AGPL-3 OCA/pms Translate me on Weblate Try me on Runboat

+

Module to connect the OCR regula with the pms

+

Table of contents

+ +
+

Usage

+

Set api key klippa and url parameters of the OCR service and select regula provider ocr in pms_property

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Commit [Sun]
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/pms project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/requirements.txt b/requirements.txt index 1fb3c55d7a..fb1fad6751 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,11 @@ # generated from manifests external_dependencies bs4 +geopy +jose +jwt +marshmallow pycountry +regula.documentreader.webclient +simplejson +thefuzz xlrd diff --git a/setup/pms_api_rest/odoo/addons/pms_api_rest b/setup/pms_api_rest/odoo/addons/pms_api_rest new file mode 120000 index 0000000000..1758ce0ed1 --- /dev/null +++ b/setup/pms_api_rest/odoo/addons/pms_api_rest @@ -0,0 +1 @@ +../../../../pms_api_rest \ No newline at end of file diff --git a/setup/pms_api_rest/setup.py b/setup/pms_api_rest/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/pms_api_rest/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/pms_ocr_klippa/odoo/addons/pms_ocr_klippa b/setup/pms_ocr_klippa/odoo/addons/pms_ocr_klippa new file mode 120000 index 0000000000..8ef547b310 --- /dev/null +++ b/setup/pms_ocr_klippa/odoo/addons/pms_ocr_klippa @@ -0,0 +1 @@ +../../../../pms_ocr_klippa \ No newline at end of file diff --git a/setup/pms_ocr_klippa/setup.py b/setup/pms_ocr_klippa/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/pms_ocr_klippa/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/pms_ocr_regula/odoo/addons/pms_ocr_regula b/setup/pms_ocr_regula/odoo/addons/pms_ocr_regula new file mode 120000 index 0000000000..9dd4c37a01 --- /dev/null +++ b/setup/pms_ocr_regula/odoo/addons/pms_ocr_regula @@ -0,0 +1 @@ +../../../../pms_ocr_regula \ No newline at end of file diff --git a/setup/pms_ocr_regula/setup.py b/setup/pms_ocr_regula/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/pms_ocr_regula/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)