Files
pms/pms_api_rest/services/pms_folio_service.py

2143 lines
93 KiB
Python

import base64
import logging
from datetime import datetime, timedelta
import pytz
from odoo import _, fields
from odoo.exceptions import 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 ..pms_api_rest_utils import url_image_pms_api_rest
_logger = logging.getLogger(__name__)
class PmsFolioService(Component):
_inherit = "base.rest.service"
_name = "pms.folio.service"
_usage = "folios"
_collection = "pms.services"
@restapi.method(
[
(
[
"/<int:folio_id>",
],
"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,
language=folio.lang if folio.lang else None,
)
else:
raise MissingError(_("Folio not found"))
@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))
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.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":
subdomains = [
[("to_assign", "=", True)],
[("state", "in", ("draft", "confirm", "arrival_delayed"))],
[("reservation_type", "!=", "out")],
]
domain_filter.append(expression.AND(subdomains))
elif folio_search_param.filterByState == "cancelled":
subdomains = [
[("state", "=", "cancel")],
]
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.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,
"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"),
)
)
return result_folios
@restapi.method(
[
(
[
"/<int:folio_id>/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(
[
(
[
"/<int:folio_id>/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"].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(
[
(
[
"/<int:folio_id>/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"].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(
[
(
[
"/<int:folio_id>/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
):
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_id.class_id.id
if reservation.room_type_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,
)
)
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.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": pms_folio_info.agencyId
if pms_folio_info.agencyId
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:
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 or "normal",
"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,
}
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:
vals_lines.append(
(
0,
0,
{
"date": reservationLine.date,
"price": reservationLine.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_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,
"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_service_ids=False,
force_write_blocked=True if external_app else False,
)._compute_board_service_room_id()
pms_folio_info.transactions = self.normalize_payments_structure(
pms_folio_info, folio
)
if pms_folio_info.transactions:
self.compute_transactions(folio, pms_folio_info.transactions)
# 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 False
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"))
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"),
]
)
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:
journal = self.env["account.journal"].search(
[("id", "=", transaction.journalId)]
)
if not journal:
ota_conf = self.env["ota.property.settings"].search(
[
("pms_property_id", "=", folio.pms_property_id.id),
("agency_id", "=", self.env.user.partner_id.id),
]
)
if ota_conf:
journal = ota_conf.pms_api_payment_journal_id
if transaction.transactionType == "inbound":
folio.do_payment(
journal,
journal.suspense_account_id,
self.env.user,
transaction.amount,
folio,
reservations=False,
services=False,
partner=False,
date=datetime.strptime(transaction.date, "%Y-%m-%d"),
ref=reference,
)
elif transaction.transactionType == "outbound":
folio.do_refund(
journal,
journal.suspense_account_id,
self.env.user,
transaction.amount,
folio,
reservations=False,
services=False,
partner=False,
date=datetime.strptime(transaction.date, "%Y-%m-%d"),
ref=reference,
)
@restapi.method(
[
(
[
"/p/<int:folio_id>",
],
"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(
[
(
[
"/<int:folio_id>/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(
[
(
[
"/<int:folio_id>/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(
[
(
[
"/<int:folio_id>/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.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(
[
(
[
"/<int:folio_id>/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(
[
(
[
"/<int:folio_id>/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,
)
)
return invoices
@restapi.method(
[
(
[
"/<int:folio_id>/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()
if not invoice_info.partnerId:
raise MissingError(_("For manual invoice, partner is required"))
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 False
@restapi.method(
[
(
[
"/<int:folio_id>/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:
# TODO change by configuration user api in the future
return (
self.env["pms.sale.channel"]
.search(
[("channel_type", "=", "direct"), ("is_on_line", "=", True)],
limit=1,
)
.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/<string:external_reference>",
],
"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),
]
)
if not folio or len(folio) > 1:
raise MissingError(_("Folio not found"))
self.update_folio_values(folio, pms_folio_info)
self.env["pms.api.log"].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",
"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",
"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 False
@restapi.method(
[
(
[
"/<int:folio_id>",
],
"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)
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",
"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",
"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 False
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.invoice_ids.filtered(lambda i: i.state == "draft")
if draft_invoices:
draft_invoices.action_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
and 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
).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_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)
# 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,
)
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),
]
)
# 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_infotransactions = [
pmsTransactionInfo(
journalId=journal.id,
transactionType="inbound",
amount=round(folio.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
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.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 vals.get("board_service_room_id"):
board = self.env["pms.board.service.room.type"].browse(
vals["board_service_room_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,
)
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
):
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()
)
if proposed_line:
vals = {}
if round(proposed_line.price, 2) != round(
line.price - board_day_price, 2
):
vals.update({"price": line.price - board_day_price})
if round(proposed_line.discount, 2) != round(line.discount, 2):
vals.update({"discount": line.discount})
if vals:
cmds.append((1, proposed_line.id, vals))
else:
cmds.append(
(
0,
False,
{
"date": line.date,
"price": line.price - board_day_price,
"discount": line.discount or 0,
},
)
)
return cmds
def wrapper_reservation_services(self, info_services, services=False):
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],
)