From a126ef7b8c981ed97c5e2de0f8f0a190a414059f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 29 Sep 2022 20:00:39 +0200 Subject: [PATCH 001/547] [ADD]l10n_es_SII: and refactoring pms documents --- pms_l10n_es_SII/README.rst | 81 ++++ pms_l10n_es_SII/__init__.py | 1 + pms_l10n_es_SII/__manifest__.py | 17 + pms_l10n_es_SII/data/pms_data.xml | 37 ++ pms_l10n_es_SII/models/__init__.py | 2 + pms_l10n_es_SII/models/res_partner.py | 69 +++ .../models/res_partner_id_category.py | 26 ++ pms_l10n_es_SII/readme/CONTRIBUTORS.rst | 3 + pms_l10n_es_SII/readme/DESCRIPTION.rst | 1 + pms_l10n_es_SII/readme/USAGE.rst | 2 + pms_l10n_es_SII/static/description/icon.png | Bin 0 -> 9455 bytes pms_l10n_es_SII/static/description/index.html | 428 ++++++++++++++++++ .../views/res_partner_id_category.xml | 15 + .../odoo/addons/pms_l10n_es_SII | 1 + setup/pms_l10n_es_SII/setup.py | 6 + 15 files changed, 689 insertions(+) create mode 100644 pms_l10n_es_SII/README.rst create mode 100644 pms_l10n_es_SII/__init__.py create mode 100644 pms_l10n_es_SII/__manifest__.py create mode 100644 pms_l10n_es_SII/data/pms_data.xml create mode 100644 pms_l10n_es_SII/models/__init__.py create mode 100644 pms_l10n_es_SII/models/res_partner.py create mode 100644 pms_l10n_es_SII/models/res_partner_id_category.py create mode 100644 pms_l10n_es_SII/readme/CONTRIBUTORS.rst create mode 100644 pms_l10n_es_SII/readme/DESCRIPTION.rst create mode 100644 pms_l10n_es_SII/readme/USAGE.rst create mode 100644 pms_l10n_es_SII/static/description/icon.png create mode 100644 pms_l10n_es_SII/static/description/index.html create mode 100644 pms_l10n_es_SII/views/res_partner_id_category.xml create mode 120000 setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII create mode 100644 setup/pms_l10n_es_SII/setup.py diff --git a/pms_l10n_es_SII/README.rst b/pms_l10n_es_SII/README.rst new file mode 100644 index 0000000000..7c8d780332 --- /dev/null +++ b/pms_l10n_es_SII/README.rst @@ -0,0 +1,81 @@ +============================== +Payment Acquirer Multiproperty +============================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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/payment_acquirer_multi_pms_properties + :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-payment_acquirer_multi_pms_properties + :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| + +Set the pms property in the payment acquirer to filter on website payments + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Sets one or more properties in the payment acquirer so that payment method is only available for documents of those properties. +If you leave it blank, it will be available to everyone. + +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] `: + + * Dario Lodeiros + +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_l10n_es_SII/__init__.py b/pms_l10n_es_SII/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pms_l10n_es_SII/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pms_l10n_es_SII/__manifest__.py b/pms_l10n_es_SII/__manifest__.py new file mode 100644 index 0000000000..a5ec4c2f21 --- /dev/null +++ b/pms_l10n_es_SII/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2009-2020 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "PMS AEAT SII Integration", + "author": "Commit [Sun], Odoo Community Association (OCA)", + "website": "https://github.com/OCA/pms", + "category": "Generic Modules/Property Management System", + "version": "14.0.1.0.2", + "license": "AGPL-3", + "depends": [ + "pms", + "l10n_es_aeat_sii_oca", + ], + "data": ["data/pms_data.xml", "views/res_partner_id_category.xml"], + "installable": True, +} diff --git a/pms_l10n_es_SII/data/pms_data.xml b/pms_l10n_es_SII/data/pms_data.xml new file mode 100644 index 0000000000..7d567f9f04 --- /dev/null +++ b/pms_l10n_es_SII/data/pms_data.xml @@ -0,0 +1,37 @@ + + + + + + True + + + + 02 + + + 05 + + + 05 + + + 03 + + + 06 + + + 06 + + + diff --git a/pms_l10n_es_SII/models/__init__.py b/pms_l10n_es_SII/models/__init__.py new file mode 100644 index 0000000000..0fc6ce7a6a --- /dev/null +++ b/pms_l10n_es_SII/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_partner_id_category +from . import res_partner diff --git a/pms_l10n_es_SII/models/res_partner.py b/pms_l10n_es_SII/models/res_partner.py new file mode 100644 index 0000000000..13d62208b3 --- /dev/null +++ b/pms_l10n_es_SII/models/res_partner.py @@ -0,0 +1,69 @@ +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + vat = fields.Char( + readonly=False, + store=True, + compute="_compute_vat", + ) + aeat_identification_type = fields.Selection( + readonly=False, + store=True, + compute="_compute_aeat_identification_type", + ) + aeat_identification = fields.Char( + readonly=False, + store=True, + compute="_compute_aeat_identification", + ) + + @api.depends( + "id_numbers", + "id_numbers.category_id", + "id_numbers.category_id.aeat_identification_type", + ) + def _compute_aeat_identification_type(self): + if hasattr(super(), "_compute_aeat_identification_type"): + super()._compute_aeat_identification_type() + for record in self: + # Passport ("03"), Residential cert. ("04") and Another document ("05") + # are setted in aeat identificacion type. + # NIF/VAT ("02") are setted in partner vat field compute + document = record.id_numbers.filtered( + lambda i: i.category_id.aeat_identification_type in ["03", "05", "06"] + ) + if document and not record.vat: + record.aeat_identification_type = document[ + 0 + ].category_id.aeat_identification_type + elif not record.aeat_identification_type or record.vat: + record.aeat_identification_type = False + + @api.depends("id_numbers", "id_numbers.name") + def _compute_aeat_identification(self): + if hasattr(super(), "_compute_aeat_identification"): + super()._compute_aeat_identification() + for record in self: + document = record.id_numbers.filtered( + lambda i: i.category_id.aeat_identification_type in ["03", "05", "06"] + ) + if document: + record.aeat_identification = document[0].name + elif not record.aeat_identification: + record.aeat_identification = False + + @api.depends("id_numbers", "id_numbers.name") + def _compute_vat(self): + if hasattr(super(), "_compute_vat"): + super()._compute_vat() + for record in self: + vat = record.id_numbers.filtered( + lambda i: i.category_id.aeat_identification_type == "02" + ) + if vat: + record.vat = vat[0].name + elif not record.vat: + record.vat = False diff --git a/pms_l10n_es_SII/models/res_partner_id_category.py b/pms_l10n_es_SII/models/res_partner_id_category.py new file mode 100644 index 0000000000..5e062de05f --- /dev/null +++ b/pms_l10n_es_SII/models/res_partner_id_category.py @@ -0,0 +1,26 @@ +from odoo import fields, models + + +class ResPartnerIdCategory(models.Model): + _inherit = "res.partner.id_category" + + aeat_identification_type = fields.Selection( + string="AEAT Identification type equivalent", + help=( + "Used to specify an identification type to send to SII. Normally for " + "sending national and export invoices to SII where the customer country " + "is not Spain, it would calculate an identification type of 04 if the VAT " + "field is filled and 06 if it was not. This field is to specify " + "types of 03 through 05, in the event that the customer doesn't identify " + "with a foreign VAT and instead with their passport " + "or residential certificate. If there is no value it will work as before." + ), + selection=[ + ("02", "NIF - VAT"), + ("03", "Passport"), + ("04", "Official document from the original country"), + ("05", "Residential certificate"), + ("06", "Another document"), + ("07", "Not registered on census"), + ], + ) diff --git a/pms_l10n_es_SII/readme/CONTRIBUTORS.rst b/pms_l10n_es_SII/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..f94c25c486 --- /dev/null +++ b/pms_l10n_es_SII/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Commit [Sun] `: + + * Dario Lodeiros diff --git a/pms_l10n_es_SII/readme/DESCRIPTION.rst b/pms_l10n_es_SII/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..1f50e77a1e --- /dev/null +++ b/pms_l10n_es_SII/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Set automatically SII fields from Checkin document Partners diff --git a/pms_l10n_es_SII/readme/USAGE.rst b/pms_l10n_es_SII/readme/USAGE.rst new file mode 100644 index 0000000000..f529c8da2b --- /dev/null +++ b/pms_l10n_es_SII/readme/USAGE.rst @@ -0,0 +1,2 @@ +When you create a new checkin document, this is copied to aeat identification type +Set the various client like a anonimous AEAT diff --git a/pms_l10n_es_SII/static/description/icon.png b/pms_l10n_es_SII/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/pms_l10n_es_SII/static/description/index.html b/pms_l10n_es_SII/static/description/index.html new file mode 100644 index 0000000000..fa8c420204 --- /dev/null +++ b/pms_l10n_es_SII/static/description/index.html @@ -0,0 +1,428 @@ + + + + + + +Payment Acquirer Multiproperty + + + +
+

Payment Acquirer Multiproperty

+ + +

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

+

Set the pms property in the payment acquirer to filter on website payments

+

Table of contents

+ +
+

Usage

+

Sets one or more properties in the payment acquirer so that payment method is only available for documents of those properties. +If you leave it blank, it will be available to everyone.

+
+
+

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] <https://www.commitsun.com>:
      +
    • Dario Lodeiros
    • +
    +
  • +
+
+
+

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_l10n_es_SII/views/res_partner_id_category.xml b/pms_l10n_es_SII/views/res_partner_id_category.xml new file mode 100644 index 0000000000..8a8ca1e8ca --- /dev/null +++ b/pms_l10n_es_SII/views/res_partner_id_category.xml @@ -0,0 +1,15 @@ + + + + res.partner.id_category + + + + + + + + diff --git a/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII b/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII new file mode 120000 index 0000000000..3a392a36a3 --- /dev/null +++ b/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII @@ -0,0 +1 @@ +../../../../pms_l10n_es_SII \ No newline at end of file diff --git a/setup/pms_l10n_es_SII/setup.py b/setup/pms_l10n_es_SII/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/pms_l10n_es_SII/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 157a266dcf2ada284501feedb944e56bbd024025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 29 Sep 2022 20:06:16 +0200 Subject: [PATCH 002/547] [ADD]l10n_es_SII: Add README --- pms_l10n_es_SII/README.rst | 81 ---- pms_l10n_es_SII/__init__.py | 1 - pms_l10n_es_SII/__manifest__.py | 17 - pms_l10n_es_SII/data/pms_data.xml | 37 -- pms_l10n_es_SII/models/__init__.py | 2 - pms_l10n_es_SII/models/res_partner.py | 69 --- .../models/res_partner_id_category.py | 26 -- pms_l10n_es_SII/readme/CONTRIBUTORS.rst | 3 - pms_l10n_es_SII/readme/DESCRIPTION.rst | 1 - pms_l10n_es_SII/readme/USAGE.rst | 2 - pms_l10n_es_SII/static/description/icon.png | Bin 9455 -> 0 bytes pms_l10n_es_SII/static/description/index.html | 428 ------------------ .../views/res_partner_id_category.xml | 15 - .../odoo/addons/pms_l10n_es_SII | 1 - setup/pms_l10n_es_SII/setup.py | 6 - 15 files changed, 689 deletions(-) delete mode 100644 pms_l10n_es_SII/README.rst delete mode 100644 pms_l10n_es_SII/__init__.py delete mode 100644 pms_l10n_es_SII/__manifest__.py delete mode 100644 pms_l10n_es_SII/data/pms_data.xml delete mode 100644 pms_l10n_es_SII/models/__init__.py delete mode 100644 pms_l10n_es_SII/models/res_partner.py delete mode 100644 pms_l10n_es_SII/models/res_partner_id_category.py delete mode 100644 pms_l10n_es_SII/readme/CONTRIBUTORS.rst delete mode 100644 pms_l10n_es_SII/readme/DESCRIPTION.rst delete mode 100644 pms_l10n_es_SII/readme/USAGE.rst delete mode 100644 pms_l10n_es_SII/static/description/icon.png delete mode 100644 pms_l10n_es_SII/static/description/index.html delete mode 100644 pms_l10n_es_SII/views/res_partner_id_category.xml delete mode 120000 setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII delete mode 100644 setup/pms_l10n_es_SII/setup.py diff --git a/pms_l10n_es_SII/README.rst b/pms_l10n_es_SII/README.rst deleted file mode 100644 index 7c8d780332..0000000000 --- a/pms_l10n_es_SII/README.rst +++ /dev/null @@ -1,81 +0,0 @@ -============================== -Payment Acquirer Multiproperty -============================== - -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! 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/payment_acquirer_multi_pms_properties - :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-payment_acquirer_multi_pms_properties - :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| - -Set the pms property in the payment acquirer to filter on website payments - -**Table of contents** - -.. contents:: - :local: - -Usage -===== - -Sets one or more properties in the payment acquirer so that payment method is only available for documents of those properties. -If you leave it blank, it will be available to everyone. - -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] `: - - * Dario Lodeiros - -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_l10n_es_SII/__init__.py b/pms_l10n_es_SII/__init__.py deleted file mode 100644 index 0650744f6b..0000000000 --- a/pms_l10n_es_SII/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import models diff --git a/pms_l10n_es_SII/__manifest__.py b/pms_l10n_es_SII/__manifest__.py deleted file mode 100644 index a5ec4c2f21..0000000000 --- a/pms_l10n_es_SII/__manifest__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2009-2020 Noviat. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -{ - "name": "PMS AEAT SII Integration", - "author": "Commit [Sun], Odoo Community Association (OCA)", - "website": "https://github.com/OCA/pms", - "category": "Generic Modules/Property Management System", - "version": "14.0.1.0.2", - "license": "AGPL-3", - "depends": [ - "pms", - "l10n_es_aeat_sii_oca", - ], - "data": ["data/pms_data.xml", "views/res_partner_id_category.xml"], - "installable": True, -} diff --git a/pms_l10n_es_SII/data/pms_data.xml b/pms_l10n_es_SII/data/pms_data.xml deleted file mode 100644 index 7d567f9f04..0000000000 --- a/pms_l10n_es_SII/data/pms_data.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - True - - - - 02 - - - 05 - - - 05 - - - 03 - - - 06 - - - 06 - - - diff --git a/pms_l10n_es_SII/models/__init__.py b/pms_l10n_es_SII/models/__init__.py deleted file mode 100644 index 0fc6ce7a6a..0000000000 --- a/pms_l10n_es_SII/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import res_partner_id_category -from . import res_partner diff --git a/pms_l10n_es_SII/models/res_partner.py b/pms_l10n_es_SII/models/res_partner.py deleted file mode 100644 index 13d62208b3..0000000000 --- a/pms_l10n_es_SII/models/res_partner.py +++ /dev/null @@ -1,69 +0,0 @@ -from odoo import api, fields, models - - -class ResPartner(models.Model): - _inherit = "res.partner" - - vat = fields.Char( - readonly=False, - store=True, - compute="_compute_vat", - ) - aeat_identification_type = fields.Selection( - readonly=False, - store=True, - compute="_compute_aeat_identification_type", - ) - aeat_identification = fields.Char( - readonly=False, - store=True, - compute="_compute_aeat_identification", - ) - - @api.depends( - "id_numbers", - "id_numbers.category_id", - "id_numbers.category_id.aeat_identification_type", - ) - def _compute_aeat_identification_type(self): - if hasattr(super(), "_compute_aeat_identification_type"): - super()._compute_aeat_identification_type() - for record in self: - # Passport ("03"), Residential cert. ("04") and Another document ("05") - # are setted in aeat identificacion type. - # NIF/VAT ("02") are setted in partner vat field compute - document = record.id_numbers.filtered( - lambda i: i.category_id.aeat_identification_type in ["03", "05", "06"] - ) - if document and not record.vat: - record.aeat_identification_type = document[ - 0 - ].category_id.aeat_identification_type - elif not record.aeat_identification_type or record.vat: - record.aeat_identification_type = False - - @api.depends("id_numbers", "id_numbers.name") - def _compute_aeat_identification(self): - if hasattr(super(), "_compute_aeat_identification"): - super()._compute_aeat_identification() - for record in self: - document = record.id_numbers.filtered( - lambda i: i.category_id.aeat_identification_type in ["03", "05", "06"] - ) - if document: - record.aeat_identification = document[0].name - elif not record.aeat_identification: - record.aeat_identification = False - - @api.depends("id_numbers", "id_numbers.name") - def _compute_vat(self): - if hasattr(super(), "_compute_vat"): - super()._compute_vat() - for record in self: - vat = record.id_numbers.filtered( - lambda i: i.category_id.aeat_identification_type == "02" - ) - if vat: - record.vat = vat[0].name - elif not record.vat: - record.vat = False diff --git a/pms_l10n_es_SII/models/res_partner_id_category.py b/pms_l10n_es_SII/models/res_partner_id_category.py deleted file mode 100644 index 5e062de05f..0000000000 --- a/pms_l10n_es_SII/models/res_partner_id_category.py +++ /dev/null @@ -1,26 +0,0 @@ -from odoo import fields, models - - -class ResPartnerIdCategory(models.Model): - _inherit = "res.partner.id_category" - - aeat_identification_type = fields.Selection( - string="AEAT Identification type equivalent", - help=( - "Used to specify an identification type to send to SII. Normally for " - "sending national and export invoices to SII where the customer country " - "is not Spain, it would calculate an identification type of 04 if the VAT " - "field is filled and 06 if it was not. This field is to specify " - "types of 03 through 05, in the event that the customer doesn't identify " - "with a foreign VAT and instead with their passport " - "or residential certificate. If there is no value it will work as before." - ), - selection=[ - ("02", "NIF - VAT"), - ("03", "Passport"), - ("04", "Official document from the original country"), - ("05", "Residential certificate"), - ("06", "Another document"), - ("07", "Not registered on census"), - ], - ) diff --git a/pms_l10n_es_SII/readme/CONTRIBUTORS.rst b/pms_l10n_es_SII/readme/CONTRIBUTORS.rst deleted file mode 100644 index f94c25c486..0000000000 --- a/pms_l10n_es_SII/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,3 +0,0 @@ -* `Commit [Sun] `: - - * Dario Lodeiros diff --git a/pms_l10n_es_SII/readme/DESCRIPTION.rst b/pms_l10n_es_SII/readme/DESCRIPTION.rst deleted file mode 100644 index 1f50e77a1e..0000000000 --- a/pms_l10n_es_SII/readme/DESCRIPTION.rst +++ /dev/null @@ -1 +0,0 @@ -Set automatically SII fields from Checkin document Partners diff --git a/pms_l10n_es_SII/readme/USAGE.rst b/pms_l10n_es_SII/readme/USAGE.rst deleted file mode 100644 index f529c8da2b..0000000000 --- a/pms_l10n_es_SII/readme/USAGE.rst +++ /dev/null @@ -1,2 +0,0 @@ -When you create a new checkin document, this is copied to aeat identification type -Set the various client like a anonimous AEAT diff --git a/pms_l10n_es_SII/static/description/icon.png b/pms_l10n_es_SII/static/description/icon.png deleted file mode 100644 index 3a0328b516c4980e8e44cdb63fd945757ddd132d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I diff --git a/pms_l10n_es_SII/static/description/index.html b/pms_l10n_es_SII/static/description/index.html deleted file mode 100644 index fa8c420204..0000000000 --- a/pms_l10n_es_SII/static/description/index.html +++ /dev/null @@ -1,428 +0,0 @@ - - - - - - -Payment Acquirer Multiproperty - - - -
-

Payment Acquirer Multiproperty

- - -

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

-

Set the pms property in the payment acquirer to filter on website payments

-

Table of contents

- -
-

Usage

-

Sets one or more properties in the payment acquirer so that payment method is only available for documents of those properties. -If you leave it blank, it will be available to everyone.

-
-
-

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] <https://www.commitsun.com>:
      -
    • Dario Lodeiros
    • -
    -
  • -
-
-
-

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_l10n_es_SII/views/res_partner_id_category.xml b/pms_l10n_es_SII/views/res_partner_id_category.xml deleted file mode 100644 index 8a8ca1e8ca..0000000000 --- a/pms_l10n_es_SII/views/res_partner_id_category.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - res.partner.id_category - - - - - - - - diff --git a/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII b/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII deleted file mode 120000 index 3a392a36a3..0000000000 --- a/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII +++ /dev/null @@ -1 +0,0 @@ -../../../../pms_l10n_es_SII \ No newline at end of file diff --git a/setup/pms_l10n_es_SII/setup.py b/setup/pms_l10n_es_SII/setup.py deleted file mode 100644 index 28c57bb640..0000000000 --- a/setup/pms_l10n_es_SII/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -import setuptools - -setuptools.setup( - setup_requires=['setuptools-odoo'], - odoo_addon=True, -) From 7b75d941f2c093257241ce43f66493026c8e19a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 6 Apr 2023 10:44:39 +0200 Subject: [PATCH 003/547] [ADD]l10n_es_SII: and refactoring pms documents --- pms_l10n_es_SII/README.rst | 81 ++++ pms_l10n_es_SII/__init__.py | 1 + pms_l10n_es_SII/__manifest__.py | 17 + pms_l10n_es_SII/data/pms_data.xml | 37 ++ pms_l10n_es_SII/models/__init__.py | 2 + pms_l10n_es_SII/models/res_partner.py | 69 +++ .../models/res_partner_id_category.py | 26 ++ pms_l10n_es_SII/readme/CONTRIBUTORS.rst | 3 + pms_l10n_es_SII/readme/DESCRIPTION.rst | 1 + pms_l10n_es_SII/readme/USAGE.rst | 2 + pms_l10n_es_SII/static/description/icon.png | Bin 0 -> 9455 bytes pms_l10n_es_SII/static/description/index.html | 428 ++++++++++++++++++ .../views/res_partner_id_category.xml | 15 + .../odoo/addons/pms_l10n_es_SII | 1 + setup/pms_l10n_es_SII/setup.py | 6 + 15 files changed, 689 insertions(+) create mode 100644 pms_l10n_es_SII/README.rst create mode 100644 pms_l10n_es_SII/__init__.py create mode 100644 pms_l10n_es_SII/__manifest__.py create mode 100644 pms_l10n_es_SII/data/pms_data.xml create mode 100644 pms_l10n_es_SII/models/__init__.py create mode 100644 pms_l10n_es_SII/models/res_partner.py create mode 100644 pms_l10n_es_SII/models/res_partner_id_category.py create mode 100644 pms_l10n_es_SII/readme/CONTRIBUTORS.rst create mode 100644 pms_l10n_es_SII/readme/DESCRIPTION.rst create mode 100644 pms_l10n_es_SII/readme/USAGE.rst create mode 100644 pms_l10n_es_SII/static/description/icon.png create mode 100644 pms_l10n_es_SII/static/description/index.html create mode 100644 pms_l10n_es_SII/views/res_partner_id_category.xml create mode 120000 setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII create mode 100644 setup/pms_l10n_es_SII/setup.py diff --git a/pms_l10n_es_SII/README.rst b/pms_l10n_es_SII/README.rst new file mode 100644 index 0000000000..7c8d780332 --- /dev/null +++ b/pms_l10n_es_SII/README.rst @@ -0,0 +1,81 @@ +============================== +Payment Acquirer Multiproperty +============================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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/payment_acquirer_multi_pms_properties + :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-payment_acquirer_multi_pms_properties + :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| + +Set the pms property in the payment acquirer to filter on website payments + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Sets one or more properties in the payment acquirer so that payment method is only available for documents of those properties. +If you leave it blank, it will be available to everyone. + +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] `: + + * Dario Lodeiros + +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_l10n_es_SII/__init__.py b/pms_l10n_es_SII/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pms_l10n_es_SII/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pms_l10n_es_SII/__manifest__.py b/pms_l10n_es_SII/__manifest__.py new file mode 100644 index 0000000000..a5ec4c2f21 --- /dev/null +++ b/pms_l10n_es_SII/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2009-2020 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "PMS AEAT SII Integration", + "author": "Commit [Sun], Odoo Community Association (OCA)", + "website": "https://github.com/OCA/pms", + "category": "Generic Modules/Property Management System", + "version": "14.0.1.0.2", + "license": "AGPL-3", + "depends": [ + "pms", + "l10n_es_aeat_sii_oca", + ], + "data": ["data/pms_data.xml", "views/res_partner_id_category.xml"], + "installable": True, +} diff --git a/pms_l10n_es_SII/data/pms_data.xml b/pms_l10n_es_SII/data/pms_data.xml new file mode 100644 index 0000000000..7d567f9f04 --- /dev/null +++ b/pms_l10n_es_SII/data/pms_data.xml @@ -0,0 +1,37 @@ + + + + + + True + + + + 02 + + + 05 + + + 05 + + + 03 + + + 06 + + + 06 + + + diff --git a/pms_l10n_es_SII/models/__init__.py b/pms_l10n_es_SII/models/__init__.py new file mode 100644 index 0000000000..0fc6ce7a6a --- /dev/null +++ b/pms_l10n_es_SII/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_partner_id_category +from . import res_partner diff --git a/pms_l10n_es_SII/models/res_partner.py b/pms_l10n_es_SII/models/res_partner.py new file mode 100644 index 0000000000..13d62208b3 --- /dev/null +++ b/pms_l10n_es_SII/models/res_partner.py @@ -0,0 +1,69 @@ +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + vat = fields.Char( + readonly=False, + store=True, + compute="_compute_vat", + ) + aeat_identification_type = fields.Selection( + readonly=False, + store=True, + compute="_compute_aeat_identification_type", + ) + aeat_identification = fields.Char( + readonly=False, + store=True, + compute="_compute_aeat_identification", + ) + + @api.depends( + "id_numbers", + "id_numbers.category_id", + "id_numbers.category_id.aeat_identification_type", + ) + def _compute_aeat_identification_type(self): + if hasattr(super(), "_compute_aeat_identification_type"): + super()._compute_aeat_identification_type() + for record in self: + # Passport ("03"), Residential cert. ("04") and Another document ("05") + # are setted in aeat identificacion type. + # NIF/VAT ("02") are setted in partner vat field compute + document = record.id_numbers.filtered( + lambda i: i.category_id.aeat_identification_type in ["03", "05", "06"] + ) + if document and not record.vat: + record.aeat_identification_type = document[ + 0 + ].category_id.aeat_identification_type + elif not record.aeat_identification_type or record.vat: + record.aeat_identification_type = False + + @api.depends("id_numbers", "id_numbers.name") + def _compute_aeat_identification(self): + if hasattr(super(), "_compute_aeat_identification"): + super()._compute_aeat_identification() + for record in self: + document = record.id_numbers.filtered( + lambda i: i.category_id.aeat_identification_type in ["03", "05", "06"] + ) + if document: + record.aeat_identification = document[0].name + elif not record.aeat_identification: + record.aeat_identification = False + + @api.depends("id_numbers", "id_numbers.name") + def _compute_vat(self): + if hasattr(super(), "_compute_vat"): + super()._compute_vat() + for record in self: + vat = record.id_numbers.filtered( + lambda i: i.category_id.aeat_identification_type == "02" + ) + if vat: + record.vat = vat[0].name + elif not record.vat: + record.vat = False diff --git a/pms_l10n_es_SII/models/res_partner_id_category.py b/pms_l10n_es_SII/models/res_partner_id_category.py new file mode 100644 index 0000000000..5e062de05f --- /dev/null +++ b/pms_l10n_es_SII/models/res_partner_id_category.py @@ -0,0 +1,26 @@ +from odoo import fields, models + + +class ResPartnerIdCategory(models.Model): + _inherit = "res.partner.id_category" + + aeat_identification_type = fields.Selection( + string="AEAT Identification type equivalent", + help=( + "Used to specify an identification type to send to SII. Normally for " + "sending national and export invoices to SII where the customer country " + "is not Spain, it would calculate an identification type of 04 if the VAT " + "field is filled and 06 if it was not. This field is to specify " + "types of 03 through 05, in the event that the customer doesn't identify " + "with a foreign VAT and instead with their passport " + "or residential certificate. If there is no value it will work as before." + ), + selection=[ + ("02", "NIF - VAT"), + ("03", "Passport"), + ("04", "Official document from the original country"), + ("05", "Residential certificate"), + ("06", "Another document"), + ("07", "Not registered on census"), + ], + ) diff --git a/pms_l10n_es_SII/readme/CONTRIBUTORS.rst b/pms_l10n_es_SII/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..f94c25c486 --- /dev/null +++ b/pms_l10n_es_SII/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Commit [Sun] `: + + * Dario Lodeiros diff --git a/pms_l10n_es_SII/readme/DESCRIPTION.rst b/pms_l10n_es_SII/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..1f50e77a1e --- /dev/null +++ b/pms_l10n_es_SII/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Set automatically SII fields from Checkin document Partners diff --git a/pms_l10n_es_SII/readme/USAGE.rst b/pms_l10n_es_SII/readme/USAGE.rst new file mode 100644 index 0000000000..f529c8da2b --- /dev/null +++ b/pms_l10n_es_SII/readme/USAGE.rst @@ -0,0 +1,2 @@ +When you create a new checkin document, this is copied to aeat identification type +Set the various client like a anonimous AEAT diff --git a/pms_l10n_es_SII/static/description/icon.png b/pms_l10n_es_SII/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/pms_l10n_es_SII/static/description/index.html b/pms_l10n_es_SII/static/description/index.html new file mode 100644 index 0000000000..fa8c420204 --- /dev/null +++ b/pms_l10n_es_SII/static/description/index.html @@ -0,0 +1,428 @@ + + + + + + +Payment Acquirer Multiproperty + + + +
+

Payment Acquirer Multiproperty

+ + +

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

+

Set the pms property in the payment acquirer to filter on website payments

+

Table of contents

+ +
+

Usage

+

Sets one or more properties in the payment acquirer so that payment method is only available for documents of those properties. +If you leave it blank, it will be available to everyone.

+
+
+

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] <https://www.commitsun.com>:
      +
    • Dario Lodeiros
    • +
    +
  • +
+
+
+

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_l10n_es_SII/views/res_partner_id_category.xml b/pms_l10n_es_SII/views/res_partner_id_category.xml new file mode 100644 index 0000000000..8a8ca1e8ca --- /dev/null +++ b/pms_l10n_es_SII/views/res_partner_id_category.xml @@ -0,0 +1,15 @@ + + + + res.partner.id_category + + + + + + + + diff --git a/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII b/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII new file mode 120000 index 0000000000..3a392a36a3 --- /dev/null +++ b/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII @@ -0,0 +1 @@ +../../../../pms_l10n_es_SII \ No newline at end of file diff --git a/setup/pms_l10n_es_SII/setup.py b/setup/pms_l10n_es_SII/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/pms_l10n_es_SII/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From d870ac0f614fd1399a6d3abd1c3c1bce1ed344e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 29 Sep 2022 20:06:16 +0200 Subject: [PATCH 004/547] [ADD]l10n_es_SII: Add README --- pms_l10n_es_SII/README.rst | 81 ---- pms_l10n_es_SII/__init__.py | 1 - pms_l10n_es_SII/__manifest__.py | 17 - pms_l10n_es_SII/data/pms_data.xml | 37 -- pms_l10n_es_SII/models/__init__.py | 2 - pms_l10n_es_SII/models/res_partner.py | 69 --- .../models/res_partner_id_category.py | 26 -- pms_l10n_es_SII/readme/CONTRIBUTORS.rst | 3 - pms_l10n_es_SII/readme/DESCRIPTION.rst | 1 - pms_l10n_es_SII/readme/USAGE.rst | 2 - pms_l10n_es_SII/static/description/icon.png | Bin 9455 -> 0 bytes pms_l10n_es_SII/static/description/index.html | 428 ------------------ .../views/res_partner_id_category.xml | 15 - .../odoo/addons/pms_l10n_es_SII | 1 - setup/pms_l10n_es_SII/setup.py | 6 - 15 files changed, 689 deletions(-) delete mode 100644 pms_l10n_es_SII/README.rst delete mode 100644 pms_l10n_es_SII/__init__.py delete mode 100644 pms_l10n_es_SII/__manifest__.py delete mode 100644 pms_l10n_es_SII/data/pms_data.xml delete mode 100644 pms_l10n_es_SII/models/__init__.py delete mode 100644 pms_l10n_es_SII/models/res_partner.py delete mode 100644 pms_l10n_es_SII/models/res_partner_id_category.py delete mode 100644 pms_l10n_es_SII/readme/CONTRIBUTORS.rst delete mode 100644 pms_l10n_es_SII/readme/DESCRIPTION.rst delete mode 100644 pms_l10n_es_SII/readme/USAGE.rst delete mode 100644 pms_l10n_es_SII/static/description/icon.png delete mode 100644 pms_l10n_es_SII/static/description/index.html delete mode 100644 pms_l10n_es_SII/views/res_partner_id_category.xml delete mode 120000 setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII delete mode 100644 setup/pms_l10n_es_SII/setup.py diff --git a/pms_l10n_es_SII/README.rst b/pms_l10n_es_SII/README.rst deleted file mode 100644 index 7c8d780332..0000000000 --- a/pms_l10n_es_SII/README.rst +++ /dev/null @@ -1,81 +0,0 @@ -============================== -Payment Acquirer Multiproperty -============================== - -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! 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/payment_acquirer_multi_pms_properties - :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-payment_acquirer_multi_pms_properties - :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| - -Set the pms property in the payment acquirer to filter on website payments - -**Table of contents** - -.. contents:: - :local: - -Usage -===== - -Sets one or more properties in the payment acquirer so that payment method is only available for documents of those properties. -If you leave it blank, it will be available to everyone. - -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] `: - - * Dario Lodeiros - -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_l10n_es_SII/__init__.py b/pms_l10n_es_SII/__init__.py deleted file mode 100644 index 0650744f6b..0000000000 --- a/pms_l10n_es_SII/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import models diff --git a/pms_l10n_es_SII/__manifest__.py b/pms_l10n_es_SII/__manifest__.py deleted file mode 100644 index a5ec4c2f21..0000000000 --- a/pms_l10n_es_SII/__manifest__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2009-2020 Noviat. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -{ - "name": "PMS AEAT SII Integration", - "author": "Commit [Sun], Odoo Community Association (OCA)", - "website": "https://github.com/OCA/pms", - "category": "Generic Modules/Property Management System", - "version": "14.0.1.0.2", - "license": "AGPL-3", - "depends": [ - "pms", - "l10n_es_aeat_sii_oca", - ], - "data": ["data/pms_data.xml", "views/res_partner_id_category.xml"], - "installable": True, -} diff --git a/pms_l10n_es_SII/data/pms_data.xml b/pms_l10n_es_SII/data/pms_data.xml deleted file mode 100644 index 7d567f9f04..0000000000 --- a/pms_l10n_es_SII/data/pms_data.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - True - - - - 02 - - - 05 - - - 05 - - - 03 - - - 06 - - - 06 - - - diff --git a/pms_l10n_es_SII/models/__init__.py b/pms_l10n_es_SII/models/__init__.py deleted file mode 100644 index 0fc6ce7a6a..0000000000 --- a/pms_l10n_es_SII/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import res_partner_id_category -from . import res_partner diff --git a/pms_l10n_es_SII/models/res_partner.py b/pms_l10n_es_SII/models/res_partner.py deleted file mode 100644 index 13d62208b3..0000000000 --- a/pms_l10n_es_SII/models/res_partner.py +++ /dev/null @@ -1,69 +0,0 @@ -from odoo import api, fields, models - - -class ResPartner(models.Model): - _inherit = "res.partner" - - vat = fields.Char( - readonly=False, - store=True, - compute="_compute_vat", - ) - aeat_identification_type = fields.Selection( - readonly=False, - store=True, - compute="_compute_aeat_identification_type", - ) - aeat_identification = fields.Char( - readonly=False, - store=True, - compute="_compute_aeat_identification", - ) - - @api.depends( - "id_numbers", - "id_numbers.category_id", - "id_numbers.category_id.aeat_identification_type", - ) - def _compute_aeat_identification_type(self): - if hasattr(super(), "_compute_aeat_identification_type"): - super()._compute_aeat_identification_type() - for record in self: - # Passport ("03"), Residential cert. ("04") and Another document ("05") - # are setted in aeat identificacion type. - # NIF/VAT ("02") are setted in partner vat field compute - document = record.id_numbers.filtered( - lambda i: i.category_id.aeat_identification_type in ["03", "05", "06"] - ) - if document and not record.vat: - record.aeat_identification_type = document[ - 0 - ].category_id.aeat_identification_type - elif not record.aeat_identification_type or record.vat: - record.aeat_identification_type = False - - @api.depends("id_numbers", "id_numbers.name") - def _compute_aeat_identification(self): - if hasattr(super(), "_compute_aeat_identification"): - super()._compute_aeat_identification() - for record in self: - document = record.id_numbers.filtered( - lambda i: i.category_id.aeat_identification_type in ["03", "05", "06"] - ) - if document: - record.aeat_identification = document[0].name - elif not record.aeat_identification: - record.aeat_identification = False - - @api.depends("id_numbers", "id_numbers.name") - def _compute_vat(self): - if hasattr(super(), "_compute_vat"): - super()._compute_vat() - for record in self: - vat = record.id_numbers.filtered( - lambda i: i.category_id.aeat_identification_type == "02" - ) - if vat: - record.vat = vat[0].name - elif not record.vat: - record.vat = False diff --git a/pms_l10n_es_SII/models/res_partner_id_category.py b/pms_l10n_es_SII/models/res_partner_id_category.py deleted file mode 100644 index 5e062de05f..0000000000 --- a/pms_l10n_es_SII/models/res_partner_id_category.py +++ /dev/null @@ -1,26 +0,0 @@ -from odoo import fields, models - - -class ResPartnerIdCategory(models.Model): - _inherit = "res.partner.id_category" - - aeat_identification_type = fields.Selection( - string="AEAT Identification type equivalent", - help=( - "Used to specify an identification type to send to SII. Normally for " - "sending national and export invoices to SII where the customer country " - "is not Spain, it would calculate an identification type of 04 if the VAT " - "field is filled and 06 if it was not. This field is to specify " - "types of 03 through 05, in the event that the customer doesn't identify " - "with a foreign VAT and instead with their passport " - "or residential certificate. If there is no value it will work as before." - ), - selection=[ - ("02", "NIF - VAT"), - ("03", "Passport"), - ("04", "Official document from the original country"), - ("05", "Residential certificate"), - ("06", "Another document"), - ("07", "Not registered on census"), - ], - ) diff --git a/pms_l10n_es_SII/readme/CONTRIBUTORS.rst b/pms_l10n_es_SII/readme/CONTRIBUTORS.rst deleted file mode 100644 index f94c25c486..0000000000 --- a/pms_l10n_es_SII/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,3 +0,0 @@ -* `Commit [Sun] `: - - * Dario Lodeiros diff --git a/pms_l10n_es_SII/readme/DESCRIPTION.rst b/pms_l10n_es_SII/readme/DESCRIPTION.rst deleted file mode 100644 index 1f50e77a1e..0000000000 --- a/pms_l10n_es_SII/readme/DESCRIPTION.rst +++ /dev/null @@ -1 +0,0 @@ -Set automatically SII fields from Checkin document Partners diff --git a/pms_l10n_es_SII/readme/USAGE.rst b/pms_l10n_es_SII/readme/USAGE.rst deleted file mode 100644 index f529c8da2b..0000000000 --- a/pms_l10n_es_SII/readme/USAGE.rst +++ /dev/null @@ -1,2 +0,0 @@ -When you create a new checkin document, this is copied to aeat identification type -Set the various client like a anonimous AEAT diff --git a/pms_l10n_es_SII/static/description/icon.png b/pms_l10n_es_SII/static/description/icon.png deleted file mode 100644 index 3a0328b516c4980e8e44cdb63fd945757ddd132d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I diff --git a/pms_l10n_es_SII/static/description/index.html b/pms_l10n_es_SII/static/description/index.html deleted file mode 100644 index fa8c420204..0000000000 --- a/pms_l10n_es_SII/static/description/index.html +++ /dev/null @@ -1,428 +0,0 @@ - - - - - - -Payment Acquirer Multiproperty - - - -
-

Payment Acquirer Multiproperty

- - -

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

-

Set the pms property in the payment acquirer to filter on website payments

-

Table of contents

- -
-

Usage

-

Sets one or more properties in the payment acquirer so that payment method is only available for documents of those properties. -If you leave it blank, it will be available to everyone.

-
-
-

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] <https://www.commitsun.com>:
      -
    • Dario Lodeiros
    • -
    -
  • -
-
-
-

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_l10n_es_SII/views/res_partner_id_category.xml b/pms_l10n_es_SII/views/res_partner_id_category.xml deleted file mode 100644 index 8a8ca1e8ca..0000000000 --- a/pms_l10n_es_SII/views/res_partner_id_category.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - res.partner.id_category - - - - - - - - diff --git a/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII b/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII deleted file mode 120000 index 3a392a36a3..0000000000 --- a/setup/pms_l10n_es_SII/odoo/addons/pms_l10n_es_SII +++ /dev/null @@ -1 +0,0 @@ -../../../../pms_l10n_es_SII \ No newline at end of file diff --git a/setup/pms_l10n_es_SII/setup.py b/setup/pms_l10n_es_SII/setup.py deleted file mode 100644 index 28c57bb640..0000000000 --- a/setup/pms_l10n_es_SII/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -import setuptools - -setuptools.setup( - setup_requires=['setuptools-odoo'], - odoo_addon=True, -) From acbba44c667b36da30916d1fad6019f0501c07d6 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 26 Jul 2021 13:07:37 +0200 Subject: [PATCH 005/547] [ADD] pms_api_rest: module created --- pms_api_rest/__init__.py | 5 + pms_api_rest/__manifest__.py | 19 +++ pms_api_rest/controllers/__init__.py | 2 + pms_api_rest/controllers/jwt_controller.py | 110 ++++++++++++++++++ pms_api_rest/controllers/pms_rest.py | 22 ++++ pms_api_rest/datamodels/__init__.py | 2 + .../pms_reservation_search_param.py | 10 ++ .../datamodels/pms_reservation_short_info.py | 16 +++ pms_api_rest/models/__init__.py | 2 + pms_api_rest/models/jwt_access_token.py | 32 +++++ pms_api_rest/models/res_users.py | 43 +++++++ pms_api_rest/security/ir.model.access.csv | 2 + pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/reservation_services.py | 51 ++++++++ pms_api_rest/static/description/icon.png | Bin 0 -> 4852 bytes 15 files changed, 317 insertions(+) create mode 100644 pms_api_rest/__init__.py create mode 100644 pms_api_rest/__manifest__.py create mode 100644 pms_api_rest/controllers/__init__.py create mode 100644 pms_api_rest/controllers/jwt_controller.py create mode 100644 pms_api_rest/controllers/pms_rest.py create mode 100644 pms_api_rest/datamodels/__init__.py create mode 100644 pms_api_rest/datamodels/pms_reservation_search_param.py create mode 100644 pms_api_rest/datamodels/pms_reservation_short_info.py create mode 100644 pms_api_rest/models/__init__.py create mode 100644 pms_api_rest/models/jwt_access_token.py create mode 100644 pms_api_rest/models/res_users.py create mode 100644 pms_api_rest/security/ir.model.access.csv create mode 100644 pms_api_rest/services/__init__.py create mode 100644 pms_api_rest/services/reservation_services.py create mode 100644 pms_api_rest/static/description/icon.png diff --git a/pms_api_rest/__init__.py b/pms_api_rest/__init__.py new file mode 100644 index 0000000000..d9df8e2dc3 --- /dev/null +++ b/pms_api_rest/__init__.py @@ -0,0 +1,5 @@ +from . import controllers +from . import datamodels +from . import models +from . import services + diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py new file mode 100644 index 0000000000..bf4ba876f3 --- /dev/null +++ b/pms_api_rest/__manifest__.py @@ -0,0 +1,19 @@ +{ + "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", + ], + "external_dependencies": { + "python": ["jwt", "simplejson", "marshmallow"], + }, + "installable": True, +} diff --git a/pms_api_rest/controllers/__init__.py b/pms_api_rest/controllers/__init__.py new file mode 100644 index 0000000000..edc7f8a32f --- /dev/null +++ b/pms_api_rest/controllers/__init__.py @@ -0,0 +1,2 @@ +from . import jwt_controller +from . import pms_rest diff --git a/pms_api_rest/controllers/jwt_controller.py b/pms_api_rest/controllers/jwt_controller.py new file mode 100644 index 0000000000..a5c544712e --- /dev/null +++ b/pms_api_rest/controllers/jwt_controller.py @@ -0,0 +1,110 @@ +import logging + +import werkzeug + +from odoo import http +from odoo.http import request + +from odoo.addons.auth_signup.models.res_users import SignupError + +from ..lib.jwt_http import jwt_http +from ..lib.validator import validator + +_logger = logging.getLogger(__name__) + +SENSITIVE_FIELDS = [ + "password", + "password_crypt", + "new_password", + "create_uid", + "write_uid", +] + + +class JwtController(http.Controller): + # test route + @http.route("/api/info", auth="public", csrf=False, cors="*") + def index(self, **kw): + return "Hello, world" + + @http.route( + "/api/login", type="http", auth="public", csrf=False, cors="*", methods=["POST"] + ) + def login(self, email, password, **kw): + return jwt_http.do_login(email, password) + + @http.route("/api/me", type="http", auth="public", csrf=False, cors="*") + def me(self, **kw): + http_method, body, headers, token = jwt_http.parse_request() + result = validator.verify_token(token) + if not result["status"]: + return jwt_http.errcode(code=result["code"], message=result["message"]) + + return jwt_http.response(request.env.user.to_dict(True)) + + @http.route("/api/logout", type="http", auth="public", csrf=False, cors="*") + def logout(self, **kw): + http_method, body, headers, token = jwt_http.parse_request() + result = validator.verify_token(token) + if not result["status"]: + return jwt_http.errcode(code=result["code"], message=result["message"]) + + jwt_http.do_logout(token) + return jwt_http.response() + + @http.route( + "/api/register", + type="http", + auth="public", + csrf=False, + cors="*", + methods=["POST"], + ) + def register(self, email=None, name=None, password=None, **kw): + if not validator.is_valid_email(email): + return jwt_http.errcode(code=400, message="Invalid email address") + if not name: + return jwt_http.errcode(code=400, message="Name cannot be empty") + if not password: + return jwt_http.errcode(code=400, message="Password cannot be empty") + + # sign up + try: + self._signup_with_values(login=email, name=name, password=password) + except AttributeError: + return jwt_http.errcode(code=501, message="Signup is disabled") + except (SignupError, AssertionError) as e: + if request.env["res.users"].sudo().search([("login", "=", email)]): + return jwt_http.errcode( + code=400, message="Email address already exists" + ) + else: + _logger.error("%s", e) + return jwt_http.response_500() + except Exception as e: + _logger.error(str(e)) + return jwt_http.response_500() + # log the user in + return jwt_http.do_login(email, password) + + def _signup_with_values(self, **values): + request.env["res.users"].sudo().signup(values, None) + request.env.cr.commit() # as authenticate will use its + # own cursor we need to commit the current transaction + self.signup_email(values) + + def signup_email(self, values): + user_sudo = ( + request.env["res.users"] + .sudo() + .search([("login", "=", values.get("login"))]) + ) + template = request.env.ref( + "auth_signup.mail_template_user_signup_account_created", + raise_if_not_found=False, + ) + if user_sudo and template: + template.sudo().with_context( + lang=user_sudo.lang, + auth_login=werkzeug.url_encode({"auth_login": user_sudo.email}), + ).send_mail(user_sudo.id, force_send=True) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py new file mode 100644 index 0000000000..1123144f8b --- /dev/null +++ b/pms_api_rest/controllers/pms_rest.py @@ -0,0 +1,22 @@ +from odoo.addons.base_rest.controllers import main + +from ..lib.jwt_http import jwt_http +from ..lib.validator import validator + + +class BaseRestDemoPublicApiController(main.RestController): + _root_path = "/api/" + _collection_name = "pms.reservation.service" + _default_auth = "public" + + # RestController OVERRIDE method + def _process_method(self, service_name, method_name, *args, params=None): + + http_method, body, headers, token = jwt_http.parse_request() + result = validator.verify_token(token) + if not result["status"]: + return jwt_http.errcode(code=result["code"], message=result["message"]) + else: + return super(BaseRestDemoPublicApiController, self)._process_method( + service_name, method_name, *args, params=params + ) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py new file mode 100644 index 0000000000..f02015c301 --- /dev/null +++ b/pms_api_rest/datamodels/__init__.py @@ -0,0 +1,2 @@ +from . import pms_reservation_short_info +from . import pms_reservation_search_param diff --git a/pms_api_rest/datamodels/pms_reservation_search_param.py b/pms_api_rest/datamodels/pms_reservation_search_param.py new file mode 100644 index 0000000000..ca2a5df107 --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation_search_param.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsReservationSearchParam(Datamodel): + _name = "pms.reservation.search.param" + + id = fields.Integer(required=False, allow_none=False) + name = fields.String(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_reservation_short_info.py b/pms_api_rest/datamodels/pms_reservation_short_info.py new file mode 100644 index 0000000000..3daa347ed8 --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation_short_info.py @@ -0,0 +1,16 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsReservationShortInfo(Datamodel): + _name = "pms.reservation.short.info" + + id = fields.Integer(required=True, allow_none=False) + partner = fields.String(required=True, allow_none=False) + checkin = fields.String(required=True, allow_none=False) + checkout = fields.String(required=True, allow_none=False) + preferred_room_id = fields.String(required=True, allow_none=False) + room_type_id = fields.String(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) + partner_requests = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py new file mode 100644 index 0000000000..eb19928999 --- /dev/null +++ b/pms_api_rest/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_users +from . import jwt_access_token diff --git a/pms_api_rest/models/jwt_access_token.py b/pms_api_rest/models/jwt_access_token.py new file mode 100644 index 0000000000..54f9fdd365 --- /dev/null +++ b/pms_api_rest/models/jwt_access_token.py @@ -0,0 +1,32 @@ +from datetime import datetime + +from odoo import api, fields, models + + +class JwtAccessToken(models.Model): + _name = "jwt_provider.access_token" + _description = "Store user access token for one-time-login" + + token = fields.Char( + "Access Token", + required=True + ) + user_id = fields.Many2one( + comodel_name="res.users", + string="User", + required=True, + ondelete="cascade", + ) + expires = fields.Datetime( + "Expires", + required=True, + ) + + is_expired = fields.Boolean( + compute="_compute_is_expired", + ) + + @api.depends("expires") + def _compute_is_expired(self): + for token in self: + token.is_expired = datetime.now() > token.expires diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py new file mode 100644 index 0000000000..65dd31fff8 --- /dev/null +++ b/pms_api_rest/models/res_users.py @@ -0,0 +1,43 @@ +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import AccessDenied, ValidationError + +from ..lib.validator import validator + +_logger = logging.getLogger(__name__) + + +class ResUsers(models.Model): + _inherit = "res.users" + + access_token_ids = fields.One2many( + string="Access Tokens", + comodel_name="jwt_provider.access_token", + inverse_name="user_id", + ) + + @classmethod + def _login(cls, db, login, password, user_agent_env): + user_id = super(ResUsers, cls)._login(db, login, password, user_agent_env) + if user_id: + return user_id + uid = validator.verify(password) + _logger.info(uid) + return uid + + @api.model + def _check_credentials(self, password, user_agent_env): + try: + super(ResUsers, self)._check_credentials(password, user_agent_env) + except AccessDenied: + if not validator.verify(password): + raise + + def to_dict(self, single=False): + res = [] + for u in self: + d = u.read(["email", "name", "company_id"])[0] + res.append(d) + + return res[0] if single else res 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..b343baac4c --- /dev/null +++ b/pms_api_rest/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 +access_jwt_access_token,Read jwt access token,model_jwt_provider_access_token,,1,0,0,0 \ No newline at end of file diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py new file mode 100644 index 0000000000..46413cba0d --- /dev/null +++ b/pms_api_rest/services/__init__.py @@ -0,0 +1 @@ +from . import reservation_services diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py new file mode 100644 index 0000000000..67536903ca --- /dev/null +++ b/pms_api_rest/services/reservation_services.py @@ -0,0 +1,51 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsReservationService(Component): + _inherit = "base.rest.service" + _name = "pms.reservation.service" + _usage = "reservations" + _collection = "pms.reservation.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET" + ) + ], + input_param=Datamodel("pms.reservation.search.param"), + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="public", + ) + def search(self, reservation_search_param): + domain = [] + if reservation_search_param.name: + domain.append(("name", "like", reservation_search_param.name)) + if reservation_search_param.id: + domain.append(("id", "=", reservation_search_param.id)) + res = [] + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + for reservation in self.env["pms.reservation"].sudo().search( + domain, + ): + res.append( + PmsReservationShortInfo( + id=reservation.id, + partner=reservation.partner_id.name, + checkin=str(reservation.checkin), + checkout=str(reservation.checkout), + preferred_room_id=reservation.preferred_room_id.name + if reservation.preferred_room_id + else "", + room_type_id=reservation.room_type_id.name + if reservation.room_type_id + else "", + name=reservation.name, + ) + ) + return res diff --git a/pms_api_rest/static/description/icon.png b/pms_api_rest/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bd2b188d63758e9984b128316e8ca6fbadc5d167 GIT binary patch literal 4852 zcmVy-`TO<#{rhK@0Q&s*W0U~+`toj@0A7&*_4w`h`tW+ABaN_6ccB>4;Ii1{ zyx!=??egA^uT+DmJaeA_wabCz?9l1(+3@w|+U3FE>dMU9udv8^)Zo2_t2LChOr5)0 z%G#!$yJd{8Ke^6}$Jm~&#es;dHowx8qrYO$-LsRlN}0G-gsLK}#c_tJLy@sx!PB3g zyi|#=Af~`|kFq=IVJz1G01^aAL_t(|oaJ5XVxq7Tu7Q9cSZ`ZIbY1Ipt6OVN_iW$) z$qr&|lgW?-2t|D2~PCN`3M(OXo^w(C+V=_kR z%nPm1E__{jm1k?dlWUUgdS2;arYqL7Jfq_=OVkQHt11&xp&L}bNj+qF#^houZEtQ- zQa3O?w{HEo|_G;4F)UM&h6(=jK^G(wdYR4?4CT87FsB4C3 zk!@XKrKAh;0#Csx^$#xeB=;)o@v|pYBA+-Z5lGpG z4L^#0Z{tCt1Ju%guhq!acu+7|{9O6XW{N>YgfyFtr=M?=K>cW~S6h{6sD8?1yKDOz z;24-r)Shq0$~#+O?QB#ocx$wNkDNmA&LExWWgRQtAFbPeR4Jz9*UzX?9Nj0%M)c?V zK>iME)s2d!PUH1MvB`;$<3Fq?Qn^Fx^EoW3EC_n5s96!-iIGkJy_?7$RhRmgy13rk zK5`7eJCPIZ-$KdLEZLW(DUi(2U~%u8VBZ&!#{EVzWvGT~DN2$hIM|q(o&aMr+De3u zX?a8QqbAz9wzGQi9m#302RY9-dI&91Fli zASDMN9k0kG7*2z@I>a1+)~0vMhFe06W zI7saRPGk{|E2LZMAwm!{TvMJR3wHE$YXDP-ZN!<I|JQF=F9a zyK6YSY9f1)Wf~P5qCHACIvga#X?{?J4C(n6dP!um76+?jA|j5)ZPn9=(}<$|`g=yd z7PGPt$?Wad=_cFVOQcS=lFl!YLn?VHN~2_@n4_fgJnS*^#lA0%@SeGiHD2#kDvGp) zaJb5kLc)(jIY83bb@|756%g_|JLxSI{=>Yc9yOGrhULj?ouuADx%0dz$TGuSXyMf%=@Ye!YBt?ay z92!aGB8d>=&@@7YNlZGzOP16vi-K0J>-{07eM}>~lg}eZ3~J;e6iuGEB^}UASOc{n z9LQtrEg`#@TbwOul2Fbsj?|u|^!hsQ@zZr9j}ehI96G8ngh?7G3T2v{*_-S>E1fGKJXc6JA33)4G+LX`q_Yg~|EDzJ67ph^!zJ z1+6d4+h3{}J~^;#KbQ{XkpD1ucZaHYv|u5_V1*F8peoNE81?@#9gX7wnEL#cXg(TLui>t$c?zCHU-Idh>&F)%)GgO)S~g$5IRes z(pl*#SaBnbbnmSpYzQ$UW*3-%aIG#2+0dn@?Q%PZt5HB z)Fj*-LLoZLOid_Bw>l;mhtRzRgiV-=`osk{Ee7+#2<;jWvOcgDLh=aA@6S3ZXtGU| z>9iwp2+K#Ga)&!(MWY_VHn7}2% zb=Y;Cyc*B+bN3%>A$0x#W}T6NWtiv{UbNW}$nzVe$Y-svzShDaFOXTMzo0f8Xbph0 z*O!W}LR7$pW-v$v3s#hq>O$cS^u>|pgajh52ivXCci3cpL9cUulFxMnH z&PYKb9ab=o^)B*(kC+87;4B^MiAA4XhH)L$rx6zj(iGNR9t{2bgCVUdwsa1$*R1O( zASPTfx1OkRV4f{ohXA6Y2I_-aT-;|~M|CE7W@lHdhY+ZXtS}lXd@Y0y7tO3=86JsU zRG!wJM+ZbQGL;V$zNM3aAe>ojJ}q-1J}am$k!*+oH=3fO0HKae1DDlAo0;aqKAv4N| z9|HXi?n?f9HH3p9!;TH4{oz#(=^Hbpm#}Se_dM(@u%)Y|5V1D?A;49ugjF;Kg zOR0Yd)4-`OM1vAOe#A-eu$Dj+Ec~K9jrL-gk6$iM!;84O;xt_4fy}M@BxfhaX@u01 zUaajKE1(ATw7(>*uVRl8=)o6BPn&_MHEZ0>1Ld+w>L_W(5aWF&tmF%!xXpFh-- z4r2cBXq^a~Wi)fZ2gbCiB?ru^Hd-YIETq?d>xr7@+Vl9#tJf>FOHD?A<%A6?#O*EC zV(3*d3AGsiFZDG4a81;Cmj$eA)2mVA5Y|xFRstdE0MFvA>fy(Kacv@WU3pf1(%au! zU)q41y3g7XYQ(>{Mi>T5&wyc{OR1axNq6`WZy`7qUjjzh zpZ}NcnE~>I*-;|wARTH9;pPxFVA7vq+`qAR&4qTY|sZ8Ez4etei{1nGMLq4j?4o(kXC z;VFA6kaxJypcy0d#po96i=iQWi&4?O7#lEtVUvZ}+e8&0yl8KeL-pP!Rr}M(JG+%2 z(4R(yQ0Ubv_C*sghsqv9>}#vH$55j1=g5Obk0G(umuiO>-zREl79pR(SA~JJ`b3G{ zXOlv`z6Sed#XetO=m=@9I3rWl>&u7Q9_>Vn^lOvf7ldEK-v0HW-ASoDHvf2ed-i(Y z<<+&SUmFYr=X6dKd+y0M{Rv3|I~n!I<8iM-&%JJXeSefd4k!i!OHA^*Uy4&#p=Vq8 zac_n?B{O6h>YFGESjDD0m^A;Oi}$`1k1WaS2Q3ynO>UxpX`<*~D%7Iow=EaHkY~?* zu_t;R2scs#(t#PZd=_GTgkx!Nm_%4%ooR%*uWy)Q@5xTC)fhCMnWTT;7yjbXf8)2Y2^OtnyYu1V^pTpogCCA&H$^ay7Q4Xk1eL_@8 zPvd+GKYBTAew^r6d8=*q{o@vIhxf==9^*ke$Q)Ik7bFTIOJRYZQ8o>pQG)Km6n`%E z)Ymxd5i#l9nkr6gncK;ZIun52$h>y21Q5_*kaU|^nEI$Z85BTvW zxVXZG<`q6*9X+=6CMN3K%$d$P7G84oLywzCRH4Wrt1RK?l~1pFwtAx>y1e;gRT9cY zy1k-xLptagTIdQOEE~+*I#f$crlMmb#2rkv~Tt=&;^j!LPKvMsc`AR}j;AlaQz8sN(%JcPn5dK-Y~x{` zs8ryb2GLvZ+kT-nNgTiZxb7;gFblRvsD(8012pNs{b@T*9bO(!|8-veY{!Xu@LmmC z5F@G+U#4y{`Z0WuBER7{pNAo{lzr?+(fjbnDACHl*SrE*vh|-8#@+RBx}5JG8PVpB#@DFQ(OOGtO=SvOHtXnFhJV z6E7y7da<`^T?OA=HdQadN2|c9H%`=5YsZRYg%{Egd6Y`HItM_*T5qpO>0GU66x1px zvFjttt3l{cJG`W{R*_`|Mj1)10N=WFO|oob8ZyS_hzhMn6dJ9Qttbke2>(5jB~ z(^MsRR{Fb(kIfueu9cz)!XOAeFSQLU%W`eo^Nt?`sX=C4+$pMwj^>H&W&ksYc5Q}` aK>q`m&r85i|Lk=D0000 Date: Mon, 26 Jul 2021 13:07:37 +0200 Subject: [PATCH 006/547] [ADD] pms_api_rest: module created --- pms_api_rest/controllers/jwt_controller.py | 4 +- pms_api_rest/controllers/pms_rest.py | 4 +- pms_api_rest/lib_jwt/jwt_http.py | 129 +++++++++++++++++++++ pms_api_rest/lib_jwt/util.py | 85 ++++++++++++++ pms_api_rest/lib_jwt/validator.py | 108 +++++++++++++++++ pms_api_rest/models/res_users.py | 2 +- 6 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 pms_api_rest/lib_jwt/jwt_http.py create mode 100644 pms_api_rest/lib_jwt/util.py create mode 100644 pms_api_rest/lib_jwt/validator.py diff --git a/pms_api_rest/controllers/jwt_controller.py b/pms_api_rest/controllers/jwt_controller.py index a5c544712e..d30237b137 100644 --- a/pms_api_rest/controllers/jwt_controller.py +++ b/pms_api_rest/controllers/jwt_controller.py @@ -7,8 +7,8 @@ from odoo.addons.auth_signup.models.res_users import SignupError -from ..lib.jwt_http import jwt_http -from ..lib.validator import validator +from ..lib_jwt.jwt_http import jwt_http +from ..lib_jwt.validator import validator _logger = logging.getLogger(__name__) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index 1123144f8b..1669215d05 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -1,7 +1,7 @@ from odoo.addons.base_rest.controllers import main -from ..lib.jwt_http import jwt_http -from ..lib.validator import validator +from ..lib_jwt.jwt_http import jwt_http +from ..lib_jwt.validator import validator class BaseRestDemoPublicApiController(main.RestController): diff --git a/pms_api_rest/lib_jwt/jwt_http.py b/pms_api_rest/lib_jwt/jwt_http.py new file mode 100644 index 0000000000..3ed66b3402 --- /dev/null +++ b/pms_api_rest/lib_jwt/jwt_http.py @@ -0,0 +1,129 @@ +import simplejson as json + +from odoo import http +from odoo.exceptions import AccessDenied +from odoo.http import Response, request + +from .validator import validator + +return_fields = ["id", "login", "name", "company_id"] + + +class JwtHttp: + def get_state(self): + return {"d": request.session.db} + + def parse_request(self): + http_method = request.httprequest.method + try: + body = http.request.params + except Exception: + body = {} + + headers = dict(list(request.httprequest.headers.items())) + if "wsgi.input" in headers: + del headers["wsgi.input"] + if "wsgi.errors" in headers: + del headers["wsgi.errors"] + if "HTTP_AUTHORIZATION" in headers: + headers["Authorization"] = headers["HTTP_AUTHORIZATION"] + + # extract token + token = "" + if "Authorization" in headers: + try: + # Bearer token_string + token = headers["Authorization"].split(" ")[1] + except Exception: + pass + + return http_method, body, headers, token + + def date2str(self, d, f="%Y-%m-%d %H:%M:%S"): + """ + Convert datetime to string + :param self: + :param d: datetime object + :param f='%Y-%m-%d%H:%M:%S': string format + """ + try: + s = d.strftime(f) + except Exception: + s = None + + return s + + def response(self, success=True, message=None, data=None, code=200): + """ + Create a HTTP Response for controller + :param success=True indicate this response is successful or not + :param message=None message string + :param data=None data to return + :param code=200 http status code + """ + print('response') + print('response') + print('response') + print('response') + payload = json.dumps( + { + "success": success, + "message": message, + "data": data, + } + ) + + return Response( + payload, + status=code, + headers=[ + ("Content-Type", "application/json"), + ], + ) + + def response_500(self, message="Internal Server Error", data=None): + return self.response(success=False, message=message, data=data, code=500) + + def response_401(self, message="401 Unauthorized", data=None): + return self.response(success=False, message=message, data=data, code=401) + + def response_404(self, message="404 Not Found", data=None): + return self.response(success=False, message=message, data=data, code=404) + + def response_403(self, message="403 Forbidden", data=None): + return self.response(success=False, message=message, data=data, code=403) + + def errcode(self, code, message=None): + return self.response(success=False, code=code, message=message) + + def do_login(self, login, password): + # get current db + state = self.get_state() + try: + uid = request.session.authenticate(state["d"], login, password) + except AccessDenied: + return self.response_401() + if not uid: + return self.response_401() + + # login success, generate token + user = request.env.user.read(return_fields)[0] + token = validator.create_token(user) + + return self.response(data={"user": user, "token": token}) + + def do_logout(self, token): + request.session.logout() + request.env["jwt_provider.access_token"].sudo().search( + [("token", "=", token)] + ).unlink() + return self.response() + + def cleanup(self): + # Clean up things after success request + # use logout here to make request as stateless as possible + request.session.logout() + return self.response() + + +jwt_http = JwtHttp() diff --git a/pms_api_rest/lib_jwt/util.py b/pms_api_rest/lib_jwt/util.py new file mode 100644 index 0000000000..8a8d465d4e --- /dev/null +++ b/pms_api_rest/lib_jwt/util.py @@ -0,0 +1,85 @@ +import logging +import os +import random +import string + +from dateutil.parser import parse + +_logger = logging.getLogger(__name__) + + +class Util: + addons_path = os.path.join(os.path.dirname(os.path.abspath(__file__))) + + def __init__(self): + self.addons_path = self.addons_path.replace("jwt_provider", "") + + def generate_verification_code(self, length=8): + return "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(length) + ) + + def toDate(self, pgTimeStr): + return parse(pgTimeStr) + + def path(self, *paths): + """Make a path""" + return os.path.join(self.addons_path, *paths) + + def add_branch(self, tree, vector, value): + """ + Given a dict, a vector, and a value, insert the value into the dict + at the tree leaf specified by the vector. Recursive! + + Params: + data (dict): The data structure to insert the vector into. + vector (list): A list of values representing the path to the leaf node. + value (object): The object to be inserted at the leaf + + Example 1: + tree = {'a': 'apple'} + vector = ['b', 'c', 'd'] + value = 'dog' + + tree = add_branch(tree, vector, value) + + Returns: + tree = { 'a': 'apple', 'b': { 'c': {'d': 'dog'}}} + + Example 2: + vector2 = ['b', 'c', 'e'] + value2 = 'egg' + + tree = add_branch(tree, vector2, value2) + + Returns: + tree = { 'a': 'apple', 'b': { 'c': {'d': 'dog', 'e': 'egg'}}} + + Returns: + dict: The dict with the value placed at the path specified. + + Algorithm: + If we're at the leaf, add it as key/value to the tree + Else: If the subtree doesn't exist, create it. + Recurse with the subtree and the left shifted vector. + Return the tree. + + """ + key = vector[0] + tree[key] = ( + value + if len(vector) == 1 + else self.add_branch(tree[key] if key in tree else {}, vector[1:], value) + ) + return tree + + def create_dict(self, d): + res = {} + for k, v in d.items(): + ar = k.split(".") + filter(None, ar) + self.add_branch(res, ar, v) + return res + + +util = Util() diff --git a/pms_api_rest/lib_jwt/validator.py b/pms_api_rest/lib_jwt/validator.py new file mode 100644 index 0000000000..9a4bbfa7bc --- /dev/null +++ b/pms_api_rest/lib_jwt/validator.py @@ -0,0 +1,108 @@ +import datetime +import logging +import re +import traceback + +import jwt +from jwt import InvalidSignatureError + +from odoo.http import request +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + +_logger = logging.getLogger(__name__) + +regex = ( + r"^[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9]" +) +regex += r"(?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" + + +class Validator: + def is_valid_email(self, email): + return re.search(regex, email) + + def key(self): + # TODO: change this key before production (build an UI Form to maintain + # (in form company?) + return "CHANGE THIS KEY" + + def create_token(self, user): + try: + exp = datetime.datetime.utcnow() + datetime.timedelta(days=30) + payload = { + "exp": exp, + "iat": datetime.datetime.utcnow(), + "sub": user["id"], + "lgn": user["login"], + } + token = jwt.encode(payload, self.key(), algorithm="HS256") + + self.save_token(token, user["id"], exp) + return token + except Exception as ex: + _logger.error(ex) + raise + + def save_token(self, token, uid, exp): + request.env["jwt_provider.access_token"].sudo().create( + { + "user_id": uid, + "expires": exp.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + "token": token, + } + ) + + def verify(self, token): + record = ( + request.env["jwt_provider.access_token"] + .sudo() + .search([("token", "=", token)]) + ) + + if len(record) != 1: + _logger.info("not found %s" % token) + return False + + if record.is_expired: + return False + + return record.user_id + + def verify_token(self, token): + try: + result = { + "status": False, + "message": None, + } + + if not self.verify(token): + result["message"] = "Token invalid or expired" + result["code"] = 498 + _logger.info("11111") + return result + + payload = jwt.decode(token, self.key(), algorithms=["HS256"]) + uid = request.session.authenticate( + request.session.db, login=payload["lgn"], password=token + ) + if not uid: + result["message"] = "Token invalid or expired" + result["code"] = 498 + _logger.info("2222") + return result + + result["status"] = True + return result + except ( + jwt.ExpiredSignatureError, + jwt.InvalidTokenError, + InvalidSignatureError, + Exception, + ): + result["code"] = 498 + result["message"] = "Token invalid or expired" + _logger.error(traceback.format_exc()) + return result + + +validator = Validator() diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index 65dd31fff8..43230f6c32 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -3,7 +3,7 @@ from odoo import _, api, fields, models from odoo.exceptions import AccessDenied, ValidationError -from ..lib.validator import validator +from ..lib_jwt.validator import validator _logger = logging.getLogger(__name__) From 68cf5844ba681840c270360744ae6776c4601a60 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 5 Aug 2021 23:06:32 +0200 Subject: [PATCH 007/547] [IMP] pms: add add exp date to jwt data --- pms_api_rest/__init__.py | 1 - pms_api_rest/controllers/pms_rest.py | 1 - .../datamodels/pms_reservation_short_info.py | 7 ++- pms_api_rest/lib_jwt/jwt_http.py | 14 +++-- pms_api_rest/lib_jwt/validator.py | 3 +- pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/jwt_access_token.py | 5 +- pms_api_rest/models/pms_reservation.py | 61 +++++++++++++++++++ pms_api_rest/models/res_users.py | 4 +- pms_api_rest/security/ir.model.access.csv | 2 +- pms_api_rest/services/reservation_services.py | 22 +++++-- requirements.txt | 3 + setup/pms_api_rest/odoo/addons/pms_api_rest | 1 + setup/pms_api_rest/setup.py | 6 ++ 14 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 pms_api_rest/models/pms_reservation.py create mode 120000 setup/pms_api_rest/odoo/addons/pms_api_rest create mode 100644 setup/pms_api_rest/setup.py diff --git a/pms_api_rest/__init__.py b/pms_api_rest/__init__.py index d9df8e2dc3..e14ece83df 100644 --- a/pms_api_rest/__init__.py +++ b/pms_api_rest/__init__.py @@ -2,4 +2,3 @@ from . import datamodels from . import models from . import services - diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index 1669215d05..0c914527bb 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -1,5 +1,4 @@ from odoo.addons.base_rest.controllers import main - from ..lib_jwt.jwt_http import jwt_http from ..lib_jwt.validator import validator diff --git a/pms_api_rest/datamodels/pms_reservation_short_info.py b/pms_api_rest/datamodels/pms_reservation_short_info.py index 3daa347ed8..db0555842f 100644 --- a/pms_api_rest/datamodels/pms_reservation_short_info.py +++ b/pms_api_rest/datamodels/pms_reservation_short_info.py @@ -10,7 +10,8 @@ class PmsReservationShortInfo(Datamodel): partner = fields.String(required=True, allow_none=False) checkin = fields.String(required=True, allow_none=False) checkout = fields.String(required=True, allow_none=False) - preferred_room_id = fields.String(required=True, allow_none=False) - room_type_id = fields.String(required=True, allow_none=False) + preferredRoomId = fields.String(required=True, allow_none=False) + roomTypeId = fields.String(required=True, allow_none=False) name = fields.String(required=True, allow_none=False) - partner_requests = fields.String(required=False, allow_none=True) + partnerRequests = fields.String(required=False, allow_none=True) + pwaActionButtons = fields.Dict(required=False, allow_none=True) diff --git a/pms_api_rest/lib_jwt/jwt_http.py b/pms_api_rest/lib_jwt/jwt_http.py index 3ed66b3402..adb20956ff 100644 --- a/pms_api_rest/lib_jwt/jwt_http.py +++ b/pms_api_rest/lib_jwt/jwt_http.py @@ -1,3 +1,5 @@ +import datetime + import simplejson as json from odoo import http @@ -61,10 +63,7 @@ def response(self, success=True, message=None, data=None, code=200): :param data=None data to return :param code=200 http status code """ - print('response') - print('response') - print('response') - print('response') + payload = json.dumps( { "success": success, @@ -108,9 +107,12 @@ def do_login(self, login, password): # login success, generate token user = request.env.user.read(return_fields)[0] - token = validator.create_token(user) + exp = datetime.datetime.utcnow() + datetime.timedelta(minutes=3) + token = validator.create_token(user, exp) - return self.response(data={"user": user, "token": token}) + return self.response( + data={"user": user, "exp": json.dumps(exp.isoformat()), "token": token} + ) def do_logout(self, token): request.session.logout() diff --git a/pms_api_rest/lib_jwt/validator.py b/pms_api_rest/lib_jwt/validator.py index 9a4bbfa7bc..b838e3d5a1 100644 --- a/pms_api_rest/lib_jwt/validator.py +++ b/pms_api_rest/lib_jwt/validator.py @@ -26,9 +26,8 @@ def key(self): # (in form company?) return "CHANGE THIS KEY" - def create_token(self, user): + def create_token(self, user, exp): try: - exp = datetime.datetime.utcnow() + datetime.timedelta(days=30) payload = { "exp": exp, "iat": datetime.datetime.utcnow(), diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index eb19928999..6cc1cdcbb3 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -1,2 +1,3 @@ from . import res_users from . import jwt_access_token +from . import pms_reservation diff --git a/pms_api_rest/models/jwt_access_token.py b/pms_api_rest/models/jwt_access_token.py index 54f9fdd365..4350e359e6 100644 --- a/pms_api_rest/models/jwt_access_token.py +++ b/pms_api_rest/models/jwt_access_token.py @@ -7,10 +7,7 @@ class JwtAccessToken(models.Model): _name = "jwt_provider.access_token" _description = "Store user access token for one-time-login" - token = fields.Char( - "Access Token", - required=True - ) + token = fields.Char("Access Token", required=True) user_id = fields.Many2one( comodel_name="res.users", string="User", diff --git a/pms_api_rest/models/pms_reservation.py b/pms_api_rest/models/pms_reservation.py new file mode 100644 index 0000000000..ae37a480c7 --- /dev/null +++ b/pms_api_rest/models/pms_reservation.py @@ -0,0 +1,61 @@ +import json + +from odoo import fields, models + + +class PmsReservation(models.Model): + _inherit = "pms.reservation" + + pwa_action_buttons = fields.Char(compute="_compute_pwa_action_buttons") + + def _compute_pwa_action_buttons(self): + """Return ordered button list, where the first button is + the preditive action, the next are active actions: + - "Assign": Predictive: Reservation by assign + Active- Idem + - "checkin": Predictive- state 'confirm' and checkin day + Active- Idem and assign + - "checkout": Predictive- Pay, onboard and checkout day + Active- Onboard and checkout day + - "Paymen": Predictive- Onboard and pending amount > 0 + Active- pending amount > 0 + - "Invoice": Predictive- qty invoice > 0, onboard, pending amount = 0 + Active- qty invoice > 0 + - "Cancel": Predictive- Never + Active- state in draft, confirm, onboard, full onboard + """ + for reservation in self: + active_buttons = {} + for k in ["Assign", "Checkin", "Checkout", "Payment", "Invoice", "Cancel"]: + if k == "Assign": + if reservation.to_assign: + active_buttons[k] = True + else: + active_buttons[k] = False + elif k == "Checkin": + if reservation.allowed_checkin: + active_buttons[k] = True + else: + active_buttons[k] = False + elif k == "Checkout": + if reservation.allowed_checkout: + active_buttons[k] = True + else: + active_buttons[k] = False + elif k == "Payment": + if reservation.folio_pending_amount > 0: + active_buttons[k] = True + else: + active_buttons[k] = False + elif k == "Invoice": + if reservation.invoice_status == "to invoice": + active_buttons[k] = True + else: + active_buttons[k] = False + elif k == "Cancel": + if reservation.allowed_cancel: + active_buttons[k] = True + else: + active_buttons[k] = False + + reservation.pwa_action_buttons = json.dumps(active_buttons) diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index 43230f6c32..b7203be401 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -1,7 +1,7 @@ import logging -from odoo import _, api, fields, models -from odoo.exceptions import AccessDenied, ValidationError +from odoo import api, fields, models +from odoo.exceptions import AccessDenied from ..lib_jwt.validator import validator diff --git a/pms_api_rest/security/ir.model.access.csv b/pms_api_rest/security/ir.model.access.csv index b343baac4c..e97ddc130e 100644 --- a/pms_api_rest/security/ir.model.access.csv +++ b/pms_api_rest/security/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_jwt_access_token,Read jwt access token,model_jwt_provider_access_token,,1,0,0,0 \ No newline at end of file +access_jwt_access_token,Read jwt access token,model_jwt_provider_access_token,,1,0,0,0 diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index 67536903ca..71f20c5671 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -1,3 +1,5 @@ +import json + from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component @@ -15,7 +17,7 @@ class PmsReservationService(Component): [ "/", ], - "GET" + "GET", ) ], input_param=Datamodel("pms.reservation.search.param"), @@ -30,8 +32,12 @@ def search(self, reservation_search_param): domain.append(("id", "=", reservation_search_param.id)) res = [] PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] - for reservation in self.env["pms.reservation"].sudo().search( - domain, + for reservation in ( + self.env["pms.reservation"] + .sudo() + .search( + domain, + ) ): res.append( PmsReservationShortInfo( @@ -39,13 +45,19 @@ def search(self, reservation_search_param): partner=reservation.partner_id.name, checkin=str(reservation.checkin), checkout=str(reservation.checkout), - preferred_room_id=reservation.preferred_room_id.name + preferredRoomId=reservation.preferred_room_id.name if reservation.preferred_room_id else "", - room_type_id=reservation.room_type_id.name + roomTypeId=reservation.room_type_id.name if reservation.room_type_id else "", name=reservation.name, + partnerRequests=reservation.partner_requests + if reservation.partner_requests + else "", + pwaActionButtons=json.loads(reservation.pwa_action_buttons) + if reservation.pwa_action_buttons + else {}, ) ) return res diff --git a/requirements.txt b/requirements.txt index 1fb3c55d7a..ba49ce1968 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ # generated from manifests external_dependencies bs4 +jwt +marshmallow pycountry +simplejson 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, +) From bd7fd502a8a65ebbf3ee61142644c6abd0398803 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 6 Aug 2021 09:41:52 +0200 Subject: [PATCH 008/547] [IMP] pms_api_rest: add cancellation flow and more --- pms_api_rest/controllers/pms_rest.py | 1 + .../datamodels/pms_reservation_short_info.py | 10 +++++- pms_api_rest/services/reservation_services.py | 32 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index 0c914527bb..1669215d05 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -1,4 +1,5 @@ from odoo.addons.base_rest.controllers import main + from ..lib_jwt.jwt_http import jwt_http from ..lib_jwt.validator import validator diff --git a/pms_api_rest/datamodels/pms_reservation_short_info.py b/pms_api_rest/datamodels/pms_reservation_short_info.py index db0555842f..71f9d886e6 100644 --- a/pms_api_rest/datamodels/pms_reservation_short_info.py +++ b/pms_api_rest/datamodels/pms_reservation_short_info.py @@ -14,4 +14,12 @@ class PmsReservationShortInfo(Datamodel): roomTypeId = fields.String(required=True, allow_none=False) name = fields.String(required=True, allow_none=False) partnerRequests = fields.String(required=False, allow_none=True) - pwaActionButtons = fields.Dict(required=False, allow_none=True) + state = fields.String(required=True, allow_none=False) + priceTotal = fields.Float(required=True, allow_none=True) + adults = fields.Integer(required=True, allow_none=False) + channelTypeId = fields.String(required=False, allow_none=True) + agencyId = fields.String(required=False, allow_none=True) + boardServiceId = fields.String(required=False, allow_none=True) + checkinsRatio = fields.Float(required=True, allow_none=False) + outstanding = fields.Float(required=True, allow_none=False) + pwaActionButtons = fields.Dict(required=True, allow_none=False) diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index 71f20c5671..e61ab368d3 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -55,9 +55,41 @@ def search(self, reservation_search_param): partnerRequests=reservation.partner_requests if reservation.partner_requests else "", + state=dict(reservation.fields_get(["state"])["state"]["selection"])[ + reservation.state + ], + priceTotal=reservation.price_total, + adults=reservation.adults, + channelTypeId=reservation.channel_type_id + if reservation.channel_type_id + else "", + agencyId=reservation.agency_id if reservation.agency_id else "", + boardServiceId=reservation.board_service_room_id.pms_board_service_id.name + if reservation.board_service_room_id + else "", + checkinsRatio=reservation.checkins_ratio, + outstanding=reservation.folio_id.pending_amount, pwaActionButtons=json.loads(reservation.pwa_action_buttons) if reservation.pwa_action_buttons else {}, ) ) return res + + @restapi.method( + [ + ( + [ + "//cancellation", + ], + "POST", + ) + ], + auth="public", + ) + def cancel_reservation(self, reservation_id): + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + if not reservation: + pass + else: + reservation.action_cancel() From c4a904603e8270ed1511c9e4fb022f2079cfd5ed Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 6 Aug 2021 14:38:10 +0200 Subject: [PATCH 009/547] [IMP] pms_api_rest: add get_reservation --- pms_api_rest/controllers/pms_rest.py | 23 +++---- pms_api_rest/services/reservation_services.py | 61 ++++++++++++++++++- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index 1669215d05..e9139ac63b 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -1,8 +1,5 @@ from odoo.addons.base_rest.controllers import main -from ..lib_jwt.jwt_http import jwt_http -from ..lib_jwt.validator import validator - class BaseRestDemoPublicApiController(main.RestController): _root_path = "/api/" @@ -10,13 +7,13 @@ class BaseRestDemoPublicApiController(main.RestController): _default_auth = "public" # RestController OVERRIDE method - def _process_method(self, service_name, method_name, *args, params=None): - - http_method, body, headers, token = jwt_http.parse_request() - result = validator.verify_token(token) - if not result["status"]: - return jwt_http.errcode(code=result["code"], message=result["message"]) - else: - return super(BaseRestDemoPublicApiController, self)._process_method( - service_name, method_name, *args, params=params - ) + # def _process_method(self, service_name, method_name, *args, params=None): + # + # http_method, body, headers, token = jwt_http.parse_request() + # result = validator.verify_token(token) + # if not result["status"]: + # return jwt_http.errcode(code=result["code"], message=result["message"]) + # else: + # return super(BaseRestDemoPublicApiController, self)._process_method( + # service_name, method_name, *args, params=params + # ) diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index e61ab368d3..35f2ada9a8 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -88,8 +88,65 @@ def search(self, reservation_search_param): auth="public", ) def cancel_reservation(self, reservation_id): - reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + reservation = ( + self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) + ) if not reservation: pass else: - reservation.action_cancel() + reservation.sudo().action_cancel() + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + auth="public", + ) + def get_reservation(self, reservation_id): + reservation = ( + self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) + ) + res = [] + if not reservation: + pass + else: + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + res = PmsReservationShortInfo( + id=reservation.id, + partner=reservation.partner_id.name, + checkin=str(reservation.checkin), + checkout=str(reservation.checkout), + preferredRoomId=reservation.preferred_room_id.name + if reservation.preferred_room_id + else "", + roomTypeId=reservation.room_type_id.name + if reservation.room_type_id + else "", + name=reservation.name, + partnerRequests=reservation.partner_requests + if reservation.partner_requests + else "", + state=dict(reservation.fields_get(["state"])["state"]["selection"])[ + reservation.state + ], + priceTotal=reservation.price_total, + adults=reservation.adults, + channelTypeId=reservation.channel_type_id + if reservation.channel_type_id + else "", + agencyId=reservation.agency_id if reservation.agency_id else "", + boardServiceId=reservation.board_service_room_id.pms_board_service_id.name + if reservation.board_service_room_id + else "", + checkinsRatio=reservation.checkins_ratio, + outstanding=reservation.folio_id.pending_amount, + pwaActionButtons=json.loads(reservation.pwa_action_buttons) + if reservation.pwa_action_buttons + else {}, + ) + return res From b674c072d1c32c2181efd6efc5ee8104d01643a5 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 31 Aug 2021 11:21:20 +0200 Subject: [PATCH 010/547] [IMP] Add folio services --- pms_api_rest/controllers/pms_rest.py | 22 ++- pms_api_rest/datamodels/__init__.py | 2 + .../datamodels/pms_folio_search_param.py | 10 + .../datamodels/pms_folio_short_info.py | 34 ++++ .../datamodels/pms_reservation_short_info.py | 26 +-- pms_api_rest/lib_jwt/jwt_http.py | 2 +- pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/folio_services.py | 179 ++++++++++++++++++ pms_api_rest/services/reservation_services.py | 73 ++++++- 9 files changed, 322 insertions(+), 27 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_folio_search_param.py create mode 100644 pms_api_rest/datamodels/pms_folio_short_info.py create mode 100644 pms_api_rest/services/folio_services.py diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index e9139ac63b..0c914527bb 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -1,4 +1,6 @@ from odoo.addons.base_rest.controllers import main +from ..lib_jwt.jwt_http import jwt_http +from ..lib_jwt.validator import validator class BaseRestDemoPublicApiController(main.RestController): @@ -7,13 +9,13 @@ class BaseRestDemoPublicApiController(main.RestController): _default_auth = "public" # RestController OVERRIDE method - # def _process_method(self, service_name, method_name, *args, params=None): - # - # http_method, body, headers, token = jwt_http.parse_request() - # result = validator.verify_token(token) - # if not result["status"]: - # return jwt_http.errcode(code=result["code"], message=result["message"]) - # else: - # return super(BaseRestDemoPublicApiController, self)._process_method( - # service_name, method_name, *args, params=params - # ) + def _process_method(self, service_name, method_name, *args, params=None): + + http_method, body, headers, token = jwt_http.parse_request() + result = validator.verify_token(token) + if not result["status"]: + return jwt_http.errcode(code=result["code"], message=result["message"]) + else: + return super(BaseRestDemoPublicApiController, self)._process_method( + service_name, method_name, *args, params=params + ) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index f02015c301..d45410026b 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -1,2 +1,4 @@ from . import pms_reservation_short_info from . import pms_reservation_search_param +from . import pms_folio_short_info +from . import pms_folio_search_param diff --git a/pms_api_rest/datamodels/pms_folio_search_param.py b/pms_api_rest/datamodels/pms_folio_search_param.py new file mode 100644 index 0000000000..027e65660b --- /dev/null +++ b/pms_api_rest/datamodels/pms_folio_search_param.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsFolioSearchParam(Datamodel): + _name = "pms.folio.search.param" + + id = fields.Integer(required=False, allow_none=False) + name = fields.String(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_folio_short_info.py b/pms_api_rest/datamodels/pms_folio_short_info.py new file mode 100644 index 0000000000..c98a1b866f --- /dev/null +++ b/pms_api_rest/datamodels/pms_folio_short_info.py @@ -0,0 +1,34 @@ +from marshmallow import fields, Schema +from typing import List +from odoo.addons.datamodel.core import Datamodel +from .pms_reservation_short_info import PmsReservationShortInfo + + +class PmsReservationSchema(Schema): + + id = fields.Integer(required=True, allow_none=False) + checkin = fields.String(required=True, allow_none=True) + checkout = fields.String(required=True, allow_none=True) + preferredRoomId = fields.String(required=False, allow_none=True) + roomTypeId = fields.String(required=False, allow_none=True) + priceTotal = fields.Float(required=False, allow_none=True) + adults = fields.Integer(required=False, allow_none=True) + pricelist = fields.String(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) + partnerName = fields.String(required=False, allow_none=True) + partnerPhone = fields.String(required=False, allow_none=True) + partnerEmail = fields.String(required=False, allow_none=True) + channelType = fields.String(required=False, allow_none=True) + agency = fields.String(required=False, allow_none=True) + # paymentState = fields.String(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + pendingAmount = fields.Float(required=False, allow_none=True) + reservations = fields.List(fields.Nested(PmsReservationSchema)) + diff --git a/pms_api_rest/datamodels/pms_reservation_short_info.py b/pms_api_rest/datamodels/pms_reservation_short_info.py index 71f9d886e6..a03852e7d2 100644 --- a/pms_api_rest/datamodels/pms_reservation_short_info.py +++ b/pms_api_rest/datamodels/pms_reservation_short_info.py @@ -7,19 +7,21 @@ class PmsReservationShortInfo(Datamodel): _name = "pms.reservation.short.info" id = fields.Integer(required=True, allow_none=False) - partner = fields.String(required=True, allow_none=False) - checkin = fields.String(required=True, allow_none=False) - checkout = fields.String(required=True, allow_none=False) - preferredRoomId = fields.String(required=True, allow_none=False) - roomTypeId = fields.String(required=True, allow_none=False) - name = fields.String(required=True, allow_none=False) + partner = fields.String(required=False, allow_none=True) + checkin = fields.String(required=True, allow_none=True) + checkout = fields.String(required=True, allow_none=True) + preferredRoomId = fields.String(required=False, allow_none=True) + roomTypeId = fields.String(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) partnerRequests = fields.String(required=False, allow_none=True) - state = fields.String(required=True, allow_none=False) - priceTotal = fields.Float(required=True, allow_none=True) - adults = fields.Integer(required=True, allow_none=False) + state = fields.String(required=False, allow_none=True) + priceTotal = fields.Float(required=False, allow_none=True) + adults = fields.Integer(required=False, allow_none=True) channelTypeId = fields.String(required=False, allow_none=True) agencyId = fields.String(required=False, allow_none=True) boardServiceId = fields.String(required=False, allow_none=True) - checkinsRatio = fields.Float(required=True, allow_none=False) - outstanding = fields.Float(required=True, allow_none=False) - pwaActionButtons = fields.Dict(required=True, allow_none=False) + checkinsRatio = fields.Float(required=False, allow_none=True) + outstanding = fields.Float(required=False, allow_none=True) + pricelist = fields.String(required=False, allow_none=True) + folioId = fields.Integer(required=False, allow_none=True) + pwaActionButtons = fields.Dict(required=False, allow_none=True) diff --git a/pms_api_rest/lib_jwt/jwt_http.py b/pms_api_rest/lib_jwt/jwt_http.py index adb20956ff..e96d144b38 100644 --- a/pms_api_rest/lib_jwt/jwt_http.py +++ b/pms_api_rest/lib_jwt/jwt_http.py @@ -107,7 +107,7 @@ def do_login(self, login, password): # login success, generate token user = request.env.user.read(return_fields)[0] - exp = datetime.datetime.utcnow() + datetime.timedelta(minutes=3) + exp = datetime.datetime.utcnow() + datetime.timedelta(minutes=30000) token = validator.create_token(user, exp) return self.response( diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 46413cba0d..ec04cb367e 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -1 +1,2 @@ from . import reservation_services +from . import folio_services diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py new file mode 100644 index 0000000000..27ffb34959 --- /dev/null +++ b/pms_api_rest/services/folio_services.py @@ -0,0 +1,179 @@ +import json + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + +from ..datamodels.pms_folio_short_info import PmsReservationSchema + + +class PmsFolioService(Component): + _inherit = "base.rest.service" + _name = "pms.folio.service" + _usage = "folios" + _collection = "pms.reservation.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.folio.search.param"), + output_param=Datamodel("pms.folio.short.info", is_list=True), + auth="public", + ) + def search(self, folio_search_param): + domain = [] + if folio_search_param.name: + domain.append(("name", "like", folio_search_param.name)) + if folio_search_param.id: + domain.append(("id", "=", folio_search_param.id)) + res = [] + PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + for folio in ( + self.env["pms.folio"] + .sudo() + .search( + domain, + ) + ): + reservations = [] + for reservation in folio.reservation_ids: + reservation_schema = PmsReservationSchema() + room = reservation.preferred_room_id.name if reservation.preferred_room_id else "" + room_type = reservation.room_type_id.name if reservation.room_type_id else "" + data = { + "id": reservation.id, + "checkin": str(reservation.checkin), + "checkout": str(reservation.checkout), + "preferredRoomId": room, + "roomTypeId": room_type, + "priceTotal": reservation.price_total, + "adults": reservation.adults, + "pricelist": reservation.pricelist_id.name, + } + reservations.append(reservation_schema.load(data)) + '''{ + "id": reservation.id, + "checkin": str(reservation.checkin), + "checkout": str(reservation.checkout), + "preferredRoomId": reservation.preferred_room_id.name + if reservation.preferred_room_id + else "", + "roomTypeId": reservation.room_type_id.name + if reservation.room_type_id + else "", + "priceTotal": reservation.price_total, + "adults": reservation.adults, + "pricelist": reservation.pricelist_id.name, + }''' + + + + + + + ''' + reservations.append( + PmsReservationSchema( + "id":reservation.id, + checkin=str(reservation.checkin), + checkout=str(reservation.checkout), + preferredRoomId=reservation.preferred_room_id.name + if reservation.preferred_room_id + else "", + roomTypeId=reservation.room_type_id.name + if reservation.room_type_id + else "", + priceTotal=reservation.price_total, + adults=reservation.adults, + pricelist=reservation.pricelist_id.name, + ) + ) + ''' + + res.append( + PmsFolioShortInfo( + id=folio.id, + name=folio.name, + partnerName=folio.partner_name if folio.partner_name else "", + partnerPhone=folio.mobile if folio.mobile else "", + partnerEmail=folio.email if folio.email else "", + channelType=folio.channel_type_id if folio.channel_type_id else "", + agency=folio.agency_id if folio.agency_id else "", + # paymentState=dict(folio.fields_get(["payment_state"])["payment_state"]["selection"])[ + # folio.payment_state + # ], + state=dict(folio.fields_get(["state"])["state"]["selection"])[ + folio.state + ], + pendingAmount=folio.pending_amount, + reservations=reservations, + ) + ) + return res + + @restapi.method( + [ + ( + [ + "//reservations", + ], + "GET", + ) + ], + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="public", + ) + def get_reservations(self, folio_id): + folio = ( + self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) + ) + res = [] + if not folio.reservation_ids: + pass + else: + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + + for reservation in folio.reservation_ids: + res.append( + PmsReservationShortInfo( + id=reservation.id, + partner=reservation.partner_id.name, + checkin=str(reservation.checkin), + checkout=str(reservation.checkout), + preferredRoomId=reservation.preferred_room_id.name + if reservation.preferred_room_id + else "", + roomTypeId=reservation.room_type_id.name + if reservation.room_type_id + else "", + name=reservation.name, + partnerRequests=reservation.partner_requests + if reservation.partner_requests + else "", + state=dict(reservation.fields_get(["state"])["state"]["selection"])[ + reservation.state + ], + priceTotal=reservation.price_total, + adults=reservation.adults, + channelTypeId=reservation.channel_type_id + if reservation.channel_type_id + else "", + agencyId=reservation.agency_id if reservation.agency_id else "", + boardServiceId=reservation.board_service_room_id.pms_board_service_id.name + if reservation.board_service_room_id + else "", + checkinsRatio=reservation.checkins_ratio, + outstanding=reservation.folio_id.pending_amount, + pwaActionButtons=json.loads(reservation.pwa_action_buttons) + if reservation.pwa_action_buttons + else {}, + ) + ) + return res diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index 35f2ada9a8..5b00cb6494 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -69,6 +69,8 @@ def search(self, reservation_search_param): else "", checkinsRatio=reservation.checkins_ratio, outstanding=reservation.folio_id.pending_amount, + pricelist=reservation.pricelist_id.name, + folioId=reservation.folio_id.id, pwaActionButtons=json.loads(reservation.pwa_action_buttons) if reservation.pwa_action_buttons else {}, @@ -105,13 +107,14 @@ def cancel_reservation(self, reservation_id): "GET", ) ], + output_param=Datamodel("pms.reservation.short.info"), auth="public", ) def get_reservation(self, reservation_id): reservation = ( self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) ) - res = [] + res = False if not reservation: pass else: @@ -145,8 +148,70 @@ def get_reservation(self, reservation_id): else "", checkinsRatio=reservation.checkins_ratio, outstanding=reservation.folio_id.pending_amount, - pwaActionButtons=json.loads(reservation.pwa_action_buttons) - if reservation.pwa_action_buttons - else {}, + pricelist=reservation.pricelist_id.name, + folioId=reservation.folio_id.id, + pwaActionButtons={}, ) return res + + @restapi.method( + [ + ( + [ + "/folio/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="public", + ) + def get_reservations(self, folio_id): + folio = ( + self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) + ) + res = [] + if not folio.reservation_ids: + pass + else: + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + + for reservation in folio.reservation_ids: + res.append( + PmsReservationShortInfo( + id=reservation.id, + partner=reservation.partner_id.name, + checkin=str(reservation.checkin), + checkout=str(reservation.checkout), + preferredRoomId=reservation.preferred_room_id.name + if reservation.preferred_room_id + else "", + roomTypeId=reservation.room_type_id.name + if reservation.room_type_id + else "", + name=reservation.name, + partnerRequests=reservation.partner_requests + if reservation.partner_requests + else "", + state=dict(reservation.fields_get(["state"])["state"]["selection"])[ + reservation.state + ], + priceTotal=reservation.price_total, + adults=reservation.adults, + channelTypeId=reservation.channel_type_id + if reservation.channel_type_id + else "", + agencyId=reservation.agency_id if reservation.agency_id else "", + boardServiceId=reservation.board_service_room_id.pms_board_service_id.name + if reservation.board_service_room_id + else "", + checkinsRatio=reservation.checkins_ratio, + outstanding=reservation.folio_id.pending_amount, + pricelist=reservation.pricelist_id.name, + folioId=reservation.folio_id.id, + pwaActionButtons=json.loads(reservation.pwa_action_buttons) + if reservation.pwa_action_buttons + else {}, + ) + ) + return res From b047ca3f531b7b627c0a23b0360c1cfb5ce8f76f Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 31 Aug 2021 18:15:29 +0200 Subject: [PATCH 011/547] [IMP] pms-pwa: service folios --- pms_api_rest/controllers/pms_rest.py | 1 + .../datamodels/pms_folio_short_info.py | 19 +---- pms_api_rest/services/folio_services.py | 78 ++++-------------- pms_api_rest/services/reservation_services.py | 65 +-------------- .../static/Models&ReservationCajonv2xD.zip | Bin 0 -> 47271 bytes 5 files changed, 24 insertions(+), 139 deletions(-) create mode 100644 pms_l10n_es/static/Models&ReservationCajonv2xD.zip diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index 0c914527bb..1669215d05 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -1,4 +1,5 @@ from odoo.addons.base_rest.controllers import main + from ..lib_jwt.jwt_http import jwt_http from ..lib_jwt.validator import validator diff --git a/pms_api_rest/datamodels/pms_folio_short_info.py b/pms_api_rest/datamodels/pms_folio_short_info.py index c98a1b866f..dad4ac0d84 100644 --- a/pms_api_rest/datamodels/pms_folio_short_info.py +++ b/pms_api_rest/datamodels/pms_folio_short_info.py @@ -1,25 +1,14 @@ -from marshmallow import fields, Schema -from typing import List +from marshmallow import Schema, fields + from odoo.addons.datamodel.core import Datamodel -from .pms_reservation_short_info import PmsReservationShortInfo class PmsReservationSchema(Schema): - id = fields.Integer(required=True, allow_none=False) - checkin = fields.String(required=True, allow_none=True) - checkout = fields.String(required=True, allow_none=True) - preferredRoomId = fields.String(required=False, allow_none=True) - roomTypeId = fields.String(required=False, allow_none=True) - priceTotal = fields.Float(required=False, allow_none=True) - adults = fields.Integer(required=False, allow_none=True) - pricelist = fields.String(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) partnerName = fields.String(required=False, allow_none=True) @@ -27,8 +16,6 @@ class PmsFolioShortInfo(Datamodel): partnerEmail = fields.String(required=False, allow_none=True) channelType = fields.String(required=False, allow_none=True) agency = fields.String(required=False, allow_none=True) - # paymentState = fields.String(required=False, allow_none=True) state = fields.String(required=False, allow_none=True) pendingAmount = fields.Float(required=False, allow_none=True) - reservations = fields.List(fields.Nested(PmsReservationSchema)) - + reservations = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 27ffb34959..779c79eedd 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -1,11 +1,7 @@ -import json - from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component -from ..datamodels.pms_folio_short_info import PmsReservationSchema - class PmsFolioService(Component): _inherit = "base.rest.service" @@ -26,78 +22,41 @@ class PmsFolioService(Component): output_param=Datamodel("pms.folio.short.info", is_list=True), auth="public", ) - def search(self, folio_search_param): + def get_folios(self, folio_search_param): domain = [] if folio_search_param.name: domain.append(("name", "like", folio_search_param.name)) if folio_search_param.id: domain.append(("id", "=", folio_search_param.id)) - res = [] + result_folios = [] PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] - PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] for folio in ( self.env["pms.folio"] - .sudo() - .search( + .sudo() + .search( domain, ) ): reservations = [] for reservation in folio.reservation_ids: - reservation_schema = PmsReservationSchema() - room = reservation.preferred_room_id.name if reservation.preferred_room_id else "" - room_type = reservation.room_type_id.name if reservation.room_type_id else "" - data = { - "id": reservation.id, - "checkin": str(reservation.checkin), - "checkout": str(reservation.checkout), - "preferredRoomId": room, - "roomTypeId": room_type, - "priceTotal": reservation.price_total, - "adults": reservation.adults, - "pricelist": reservation.pricelist_id.name, - } - reservations.append(reservation_schema.load(data)) - '''{ - "id": reservation.id, - "checkin": str(reservation.checkin), - "checkout": str(reservation.checkout), - "preferredRoomId": reservation.preferred_room_id.name - if reservation.preferred_room_id - else "", - "roomTypeId": reservation.room_type_id.name - if reservation.room_type_id - else "", - "priceTotal": reservation.price_total, - "adults": reservation.adults, - "pricelist": reservation.pricelist_id.name, - }''' - - - - - - - ''' reservations.append( - PmsReservationSchema( - "id":reservation.id, - checkin=str(reservation.checkin), - checkout=str(reservation.checkout), - preferredRoomId=reservation.preferred_room_id.name + { + "id": reservation.id, + "checkin": str(reservation.checkin), + "checkout": str(reservation.checkout), + "preferredRoomId": reservation.preferred_room_id.name if reservation.preferred_room_id else "", - roomTypeId=reservation.room_type_id.name + "roomTypeId": reservation.room_type_id.name if reservation.room_type_id else "", - priceTotal=reservation.price_total, - adults=reservation.adults, - pricelist=reservation.pricelist_id.name, - ) + "priceTotal": reservation.price_total, + "adults": reservation.adults, + "pricelist": reservation.pricelist_id.name, + } ) - ''' - res.append( + result_folios.append( PmsFolioShortInfo( id=folio.id, name=folio.name, @@ -106,17 +65,14 @@ def search(self, folio_search_param): partnerEmail=folio.email if folio.email else "", channelType=folio.channel_type_id if folio.channel_type_id else "", agency=folio.agency_id if folio.agency_id else "", - # paymentState=dict(folio.fields_get(["payment_state"])["payment_state"]["selection"])[ - # folio.payment_state - # ], state=dict(folio.fields_get(["state"])["state"]["selection"])[ folio.state ], pendingAmount=folio.pending_amount, - reservations=reservations, + reservations=[] if not reservations else reservations, ) ) - return res + return result_folios @restapi.method( [ diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index 5b00cb6494..264b304e2d 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -11,6 +11,7 @@ class PmsReservationService(Component): _usage = "reservations" _collection = "pms.reservation.service" + # TODO: REMOVE @restapi.method( [ ( @@ -24,7 +25,7 @@ class PmsReservationService(Component): output_param=Datamodel("pms.reservation.short.info", is_list=True), auth="public", ) - def search(self, reservation_search_param): + def get_reservations(self, reservation_search_param): domain = [] if reservation_search_param.name: domain.append(("name", "like", reservation_search_param.name)) @@ -77,6 +78,7 @@ def search(self, reservation_search_param): ) ) return res + # END TODO: REMOVE @restapi.method( [ @@ -154,64 +156,3 @@ def get_reservation(self, reservation_id): ) return res - @restapi.method( - [ - ( - [ - "/folio/", - ], - "GET", - ) - ], - output_param=Datamodel("pms.reservation.short.info", is_list=True), - auth="public", - ) - def get_reservations(self, folio_id): - folio = ( - self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) - ) - res = [] - if not folio.reservation_ids: - pass - else: - PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] - - for reservation in folio.reservation_ids: - res.append( - PmsReservationShortInfo( - id=reservation.id, - partner=reservation.partner_id.name, - checkin=str(reservation.checkin), - checkout=str(reservation.checkout), - preferredRoomId=reservation.preferred_room_id.name - if reservation.preferred_room_id - else "", - roomTypeId=reservation.room_type_id.name - if reservation.room_type_id - else "", - name=reservation.name, - partnerRequests=reservation.partner_requests - if reservation.partner_requests - else "", - state=dict(reservation.fields_get(["state"])["state"]["selection"])[ - reservation.state - ], - priceTotal=reservation.price_total, - adults=reservation.adults, - channelTypeId=reservation.channel_type_id - if reservation.channel_type_id - else "", - agencyId=reservation.agency_id if reservation.agency_id else "", - boardServiceId=reservation.board_service_room_id.pms_board_service_id.name - if reservation.board_service_room_id - else "", - checkinsRatio=reservation.checkins_ratio, - outstanding=reservation.folio_id.pending_amount, - pricelist=reservation.pricelist_id.name, - folioId=reservation.folio_id.id, - pwaActionButtons=json.loads(reservation.pwa_action_buttons) - if reservation.pwa_action_buttons - else {}, - ) - ) - return res diff --git a/pms_l10n_es/static/Models&ReservationCajonv2xD.zip b/pms_l10n_es/static/Models&ReservationCajonv2xD.zip new file mode 100644 index 0000000000000000000000000000000000000000..a322610516e92187754ebd4e147a59d8c38a2b90 GIT binary patch literal 47271 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 z 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 R|JVommrDPYmu>%P{TGeQ^f~|l literal 0 HcmV?d00001 From 95e5eaa278edec9c0d4deb6020c2f73cea11c57a Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 3 Sep 2021 11:14:46 +0200 Subject: [PATCH 012/547] [IMP] Change date format in checkin and checkout --- pms_api_rest/services/folio_services.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 779c79eedd..3da29c61f6 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -1,6 +1,7 @@ from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component +from datetime import datetime class PmsFolioService(Component): @@ -32,8 +33,8 @@ def get_folios(self, folio_search_param): PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] for folio in ( self.env["pms.folio"] - .sudo() - .search( + .sudo() + .search( domain, ) ): @@ -42,8 +43,8 @@ def get_folios(self, folio_search_param): reservations.append( { "id": reservation.id, - "checkin": str(reservation.checkin), - "checkout": str(reservation.checkout), + "checkin": datetime.combine(reservation.checkin, datetime.min.time()).isoformat(), + "checkout": datetime.combine(reservation.checkout, datetime.min.time()).isoformat(), "preferredRoomId": reservation.preferred_room_id.name if reservation.preferred_room_id else "", @@ -53,6 +54,9 @@ def get_folios(self, folio_search_param): "priceTotal": reservation.price_total, "adults": reservation.adults, "pricelist": reservation.pricelist_id.name, + "boardService": reservation.board_service_room_id.pms_board_service_id.name + if reservation.board_service_room_id + else "", } ) From 7430841d3737c4d196fd0ace697badfab2bbec2a Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 8 Sep 2021 12:46:21 +0200 Subject: [PATCH 013/547] [IMP] Pms_api_rest: add sales person in folio_short_info --- pms_api_rest/datamodels/pms_folio_short_info.py | 3 ++- pms_api_rest/services/folio_services.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio_short_info.py b/pms_api_rest/datamodels/pms_folio_short_info.py index dad4ac0d84..ade9d172af 100644 --- a/pms_api_rest/datamodels/pms_folio_short_info.py +++ b/pms_api_rest/datamodels/pms_folio_short_info.py @@ -14,8 +14,9 @@ class PmsFolioShortInfo(Datamodel): partnerName = fields.String(required=False, allow_none=True) partnerPhone = fields.String(required=False, allow_none=True) partnerEmail = fields.String(required=False, allow_none=True) - channelType = fields.String(required=False, allow_none=True) + saleChannel = fields.String(required=False, allow_none=True) agency = fields.String(required=False, allow_none=True) state = fields.String(required=False, allow_none=True) pendingAmount = fields.Float(required=False, allow_none=True) reservations = fields.List(fields.Dict(required=False, allow_none=True)) + salesPerson = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 3da29c61f6..15291dfd9e 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -59,7 +59,6 @@ def get_folios(self, folio_search_param): else "", } ) - result_folios.append( PmsFolioShortInfo( id=folio.id, @@ -67,13 +66,14 @@ def get_folios(self, folio_search_param): partnerName=folio.partner_name if folio.partner_name else "", partnerPhone=folio.mobile if folio.mobile else "", partnerEmail=folio.email if folio.email else "", - channelType=folio.channel_type_id if folio.channel_type_id else "", - agency=folio.agency_id if folio.agency_id else "", + saleChannel=folio.channel_type_id.name if folio.channel_type_id else "", + agency=folio.agency_id.name if folio.agency_id else "", state=dict(folio.fields_get(["state"])["state"]["selection"])[ folio.state ], pendingAmount=folio.pending_amount, reservations=[] if not reservations else reservations, + salesPerson=folio.user_id.name if folio.user_id else "", ) ) return result_folios @@ -122,10 +122,10 @@ def get_reservations(self, folio_id): ], priceTotal=reservation.price_total, adults=reservation.adults, - channelTypeId=reservation.channel_type_id + channelTypeId=reservation.channel_type_id.name if reservation.channel_type_id else "", - agencyId=reservation.agency_id if reservation.agency_id else "", + agencyId=reservation.agency_id.name if reservation.agency_id else "", boardServiceId=reservation.board_service_room_id.pms_board_service_id.name if reservation.board_service_room_id else "", From 7f7768a562cabdad0cf9d217487765fd20c311f1 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 21 Sep 2021 09:32:06 +0200 Subject: [PATCH 014/547] [ADD] pms-pwa: nested reservation lines --- pms_api_rest/datamodels/__init__.py | 4 +- .../datamodels/pms_folio_short_info.py | 6 +- .../datamodels/pms_reservation_short_info.py | 27 --- ...arch_param.py => pms_room_search_param.py} | 4 +- .../datamodels/pms_room_short_info.py | 9 + pms_api_rest/models/__init__.py | 1 - pms_api_rest/models/pms_reservation.py | 61 ------- pms_api_rest/services/__init__.py | 2 +- pms_api_rest/services/folio_services.py | 47 ++++-- pms_api_rest/services/reservation_services.py | 158 ------------------ pms_api_rest/services/room_services.py | 49 ++++++ 11 files changed, 95 insertions(+), 273 deletions(-) delete mode 100644 pms_api_rest/datamodels/pms_reservation_short_info.py rename pms_api_rest/datamodels/{pms_reservation_search_param.py => pms_room_search_param.py} (69%) create mode 100644 pms_api_rest/datamodels/pms_room_short_info.py delete mode 100644 pms_api_rest/models/pms_reservation.py delete mode 100644 pms_api_rest/services/reservation_services.py create mode 100644 pms_api_rest/services/room_services.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index d45410026b..0b73cc6166 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -1,4 +1,4 @@ -from . import pms_reservation_short_info -from . import pms_reservation_search_param from . import pms_folio_short_info from . import pms_folio_search_param +from . import pms_room_short_info +from . import pms_room_search_param diff --git a/pms_api_rest/datamodels/pms_folio_short_info.py b/pms_api_rest/datamodels/pms_folio_short_info.py index ade9d172af..9c79653c5f 100644 --- a/pms_api_rest/datamodels/pms_folio_short_info.py +++ b/pms_api_rest/datamodels/pms_folio_short_info.py @@ -1,12 +1,8 @@ -from marshmallow import Schema, fields +from marshmallow import fields from odoo.addons.datamodel.core import Datamodel -class PmsReservationSchema(Schema): - id = fields.Integer(required=True, allow_none=False) - - class PmsFolioShortInfo(Datamodel): _name = "pms.folio.short.info" id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_reservation_short_info.py b/pms_api_rest/datamodels/pms_reservation_short_info.py deleted file mode 100644 index a03852e7d2..0000000000 --- a/pms_api_rest/datamodels/pms_reservation_short_info.py +++ /dev/null @@ -1,27 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsReservationShortInfo(Datamodel): - _name = "pms.reservation.short.info" - - id = fields.Integer(required=True, allow_none=False) - partner = fields.String(required=False, allow_none=True) - checkin = fields.String(required=True, allow_none=True) - checkout = fields.String(required=True, allow_none=True) - preferredRoomId = fields.String(required=False, allow_none=True) - roomTypeId = fields.String(required=False, allow_none=True) - name = fields.String(required=False, allow_none=True) - partnerRequests = fields.String(required=False, allow_none=True) - state = fields.String(required=False, allow_none=True) - priceTotal = fields.Float(required=False, allow_none=True) - adults = fields.Integer(required=False, allow_none=True) - channelTypeId = fields.String(required=False, allow_none=True) - agencyId = fields.String(required=False, allow_none=True) - boardServiceId = fields.String(required=False, allow_none=True) - checkinsRatio = fields.Float(required=False, allow_none=True) - outstanding = fields.Float(required=False, allow_none=True) - pricelist = fields.String(required=False, allow_none=True) - folioId = fields.Integer(required=False, allow_none=True) - pwaActionButtons = fields.Dict(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_reservation_search_param.py b/pms_api_rest/datamodels/pms_room_search_param.py similarity index 69% rename from pms_api_rest/datamodels/pms_reservation_search_param.py rename to pms_api_rest/datamodels/pms_room_search_param.py index ca2a5df107..8945237e23 100644 --- a/pms_api_rest/datamodels/pms_reservation_search_param.py +++ b/pms_api_rest/datamodels/pms_room_search_param.py @@ -3,8 +3,8 @@ from odoo.addons.datamodel.core import Datamodel -class PmsReservationSearchParam(Datamodel): - _name = "pms.reservation.search.param" +class PmsRoomSearchParam(Datamodel): + _name = "pms.room.search.param" id = fields.Integer(required=False, allow_none=False) name = fields.String(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_room_short_info.py b/pms_api_rest/datamodels/pms_room_short_info.py new file mode 100644 index 0000000000..b69b22e535 --- /dev/null +++ b/pms_api_rest/datamodels/pms_room_short_info.py @@ -0,0 +1,9 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsRoomShortInfo(Datamodel): + _name = "pms.room.short.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 6cc1cdcbb3..eb19928999 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -1,3 +1,2 @@ from . import res_users from . import jwt_access_token -from . import pms_reservation diff --git a/pms_api_rest/models/pms_reservation.py b/pms_api_rest/models/pms_reservation.py deleted file mode 100644 index ae37a480c7..0000000000 --- a/pms_api_rest/models/pms_reservation.py +++ /dev/null @@ -1,61 +0,0 @@ -import json - -from odoo import fields, models - - -class PmsReservation(models.Model): - _inherit = "pms.reservation" - - pwa_action_buttons = fields.Char(compute="_compute_pwa_action_buttons") - - def _compute_pwa_action_buttons(self): - """Return ordered button list, where the first button is - the preditive action, the next are active actions: - - "Assign": Predictive: Reservation by assign - Active- Idem - - "checkin": Predictive- state 'confirm' and checkin day - Active- Idem and assign - - "checkout": Predictive- Pay, onboard and checkout day - Active- Onboard and checkout day - - "Paymen": Predictive- Onboard and pending amount > 0 - Active- pending amount > 0 - - "Invoice": Predictive- qty invoice > 0, onboard, pending amount = 0 - Active- qty invoice > 0 - - "Cancel": Predictive- Never - Active- state in draft, confirm, onboard, full onboard - """ - for reservation in self: - active_buttons = {} - for k in ["Assign", "Checkin", "Checkout", "Payment", "Invoice", "Cancel"]: - if k == "Assign": - if reservation.to_assign: - active_buttons[k] = True - else: - active_buttons[k] = False - elif k == "Checkin": - if reservation.allowed_checkin: - active_buttons[k] = True - else: - active_buttons[k] = False - elif k == "Checkout": - if reservation.allowed_checkout: - active_buttons[k] = True - else: - active_buttons[k] = False - elif k == "Payment": - if reservation.folio_pending_amount > 0: - active_buttons[k] = True - else: - active_buttons[k] = False - elif k == "Invoice": - if reservation.invoice_status == "to invoice": - active_buttons[k] = True - else: - active_buttons[k] = False - elif k == "Cancel": - if reservation.allowed_cancel: - active_buttons[k] = True - else: - active_buttons[k] = False - - reservation.pwa_action_buttons = json.dumps(active_buttons) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index ec04cb367e..6329f4b991 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -1,2 +1,2 @@ -from . import reservation_services from . import folio_services +from . import room_services diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 15291dfd9e..a78c13a1f3 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -1,7 +1,8 @@ +from datetime import datetime + from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component -from datetime import datetime class PmsFolioService(Component): @@ -33,18 +34,32 @@ def get_folios(self, folio_search_param): PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] for folio in ( self.env["pms.folio"] - .sudo() - .search( + .sudo() + .search( domain, ) ): reservations = [] for reservation in folio.reservation_ids: + reservation_lines = [] + for reservation_line in reservation.reservation_line_ids: + reservation_lines.append( + { + "id": reservation_line.id, + "date": reservation_line.date, + "roomId": reservation_line.room_id.id, + } + ) + reservations.append( { "id": reservation.id, - "checkin": datetime.combine(reservation.checkin, datetime.min.time()).isoformat(), - "checkout": datetime.combine(reservation.checkout, datetime.min.time()).isoformat(), + "checkin": datetime.combine( + reservation.checkin, datetime.min.time() + ).isoformat(), + "checkout": datetime.combine( + reservation.checkout, datetime.min.time() + ).isoformat(), "preferredRoomId": reservation.preferred_room_id.name if reservation.preferred_room_id else "", @@ -57,6 +72,7 @@ def get_folios(self, folio_search_param): "boardService": reservation.board_service_room_id.pms_board_service_id.name if reservation.board_service_room_id else "", + "reservationLines": [] if not reservation_lines else reservation_lines } ) result_folios.append( @@ -66,7 +82,9 @@ def get_folios(self, folio_search_param): partnerName=folio.partner_name if folio.partner_name else "", partnerPhone=folio.mobile if folio.mobile else "", partnerEmail=folio.email if folio.email else "", - saleChannel=folio.channel_type_id.name if folio.channel_type_id else "", + saleChannel=folio.channel_type_id.name + if folio.channel_type_id + else "", agency=folio.agency_id.name if folio.agency_id else "", state=dict(folio.fields_get(["state"])["state"]["selection"])[ folio.state @@ -91,9 +109,7 @@ def get_folios(self, folio_search_param): auth="public", ) def get_reservations(self, folio_id): - folio = ( - self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) - ) + folio = self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) res = [] if not folio.reservation_ids: pass @@ -117,23 +133,22 @@ def get_reservations(self, folio_id): partnerRequests=reservation.partner_requests if reservation.partner_requests else "", - state=dict(reservation.fields_get(["state"])["state"]["selection"])[ - reservation.state - ], + state=dict( + reservation.fields_get(["state"])["state"]["selection"] + )[reservation.state], priceTotal=reservation.price_total, adults=reservation.adults, channelTypeId=reservation.channel_type_id.name if reservation.channel_type_id else "", - agencyId=reservation.agency_id.name if reservation.agency_id else "", + agencyId=reservation.agency_id.name + if reservation.agency_id + else "", boardServiceId=reservation.board_service_room_id.pms_board_service_id.name if reservation.board_service_room_id else "", checkinsRatio=reservation.checkins_ratio, outstanding=reservation.folio_id.pending_amount, - pwaActionButtons=json.loads(reservation.pwa_action_buttons) - if reservation.pwa_action_buttons - else {}, ) ) return res diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py deleted file mode 100644 index 264b304e2d..0000000000 --- a/pms_api_rest/services/reservation_services.py +++ /dev/null @@ -1,158 +0,0 @@ -import json - -from odoo.addons.base_rest import restapi -from odoo.addons.base_rest_datamodel.restapi import Datamodel -from odoo.addons.component.core import Component - - -class PmsReservationService(Component): - _inherit = "base.rest.service" - _name = "pms.reservation.service" - _usage = "reservations" - _collection = "pms.reservation.service" - - # TODO: REMOVE - @restapi.method( - [ - ( - [ - "/", - ], - "GET", - ) - ], - input_param=Datamodel("pms.reservation.search.param"), - output_param=Datamodel("pms.reservation.short.info", is_list=True), - auth="public", - ) - def get_reservations(self, reservation_search_param): - domain = [] - if reservation_search_param.name: - domain.append(("name", "like", reservation_search_param.name)) - if reservation_search_param.id: - domain.append(("id", "=", reservation_search_param.id)) - res = [] - PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] - for reservation in ( - self.env["pms.reservation"] - .sudo() - .search( - domain, - ) - ): - res.append( - PmsReservationShortInfo( - id=reservation.id, - partner=reservation.partner_id.name, - checkin=str(reservation.checkin), - checkout=str(reservation.checkout), - preferredRoomId=reservation.preferred_room_id.name - if reservation.preferred_room_id - else "", - roomTypeId=reservation.room_type_id.name - if reservation.room_type_id - else "", - name=reservation.name, - partnerRequests=reservation.partner_requests - if reservation.partner_requests - else "", - state=dict(reservation.fields_get(["state"])["state"]["selection"])[ - reservation.state - ], - priceTotal=reservation.price_total, - adults=reservation.adults, - channelTypeId=reservation.channel_type_id - if reservation.channel_type_id - else "", - agencyId=reservation.agency_id if reservation.agency_id else "", - boardServiceId=reservation.board_service_room_id.pms_board_service_id.name - if reservation.board_service_room_id - else "", - checkinsRatio=reservation.checkins_ratio, - outstanding=reservation.folio_id.pending_amount, - pricelist=reservation.pricelist_id.name, - folioId=reservation.folio_id.id, - pwaActionButtons=json.loads(reservation.pwa_action_buttons) - if reservation.pwa_action_buttons - else {}, - ) - ) - return res - # END TODO: REMOVE - - @restapi.method( - [ - ( - [ - "//cancellation", - ], - "POST", - ) - ], - auth="public", - ) - def cancel_reservation(self, reservation_id): - reservation = ( - self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) - ) - if not reservation: - pass - else: - reservation.sudo().action_cancel() - - @restapi.method( - [ - ( - [ - "/", - ], - "GET", - ) - ], - output_param=Datamodel("pms.reservation.short.info"), - auth="public", - ) - def get_reservation(self, reservation_id): - reservation = ( - self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) - ) - res = False - if not reservation: - pass - else: - PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] - res = PmsReservationShortInfo( - id=reservation.id, - partner=reservation.partner_id.name, - checkin=str(reservation.checkin), - checkout=str(reservation.checkout), - preferredRoomId=reservation.preferred_room_id.name - if reservation.preferred_room_id - else "", - roomTypeId=reservation.room_type_id.name - if reservation.room_type_id - else "", - name=reservation.name, - partnerRequests=reservation.partner_requests - if reservation.partner_requests - else "", - state=dict(reservation.fields_get(["state"])["state"]["selection"])[ - reservation.state - ], - priceTotal=reservation.price_total, - adults=reservation.adults, - channelTypeId=reservation.channel_type_id - if reservation.channel_type_id - else "", - agencyId=reservation.agency_id if reservation.agency_id else "", - boardServiceId=reservation.board_service_room_id.pms_board_service_id.name - if reservation.board_service_room_id - else "", - checkinsRatio=reservation.checkins_ratio, - outstanding=reservation.folio_id.pending_amount, - pricelist=reservation.pricelist_id.name, - folioId=reservation.folio_id.id, - pwaActionButtons={}, - ) - return res - diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py new file mode 100644 index 0000000000..787d9d8515 --- /dev/null +++ b/pms_api_rest/services/room_services.py @@ -0,0 +1,49 @@ +from datetime import datetime + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsFolioService(Component): + _inherit = "base.rest.service" + _name = "pms.room.service" + _usage = "rooms" + _collection = "pms.reservation.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.folio.search.param"), + output_param=Datamodel("pms.folio.short.info", is_list=True), + auth="public", + ) + 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.id: + domain.append(("id", "=", room_search_param.id)) + result_rooms = [] + PmsRoomShortInfo = self.env.datamodels["pms.room.short.info"] + for room in ( + self.env["pms.room"] + .sudo() + .search( + domain, + ) + ): + + result_rooms.append( + PmsRoomShortInfo( + id=room.id, + name=room.name, + ) + ) + return result_rooms From b49339bb36b8d4f3db7d852ca846a14bc1619ef3 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 22 Sep 2021 18:19:37 +0200 Subject: [PATCH 015/547] [ADD] pms-pwa: add calendar service --- pms_api_rest/datamodels/__init__.py | 6 ++- .../datamodels/pms_calendar_search_param.py | 9 ++++ .../datamodels/pms_calendar_short_info.py | 12 +++++ .../datamodels/pms_room_search_param.py | 5 +- pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/calendar_service.py | 49 +++++++++++++++++++ pms_api_rest/services/folio_services.py | 6 ++- pms_api_rest/services/room_services.py | 8 ++- 8 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_calendar_search_param.py create mode 100644 pms_api_rest/datamodels/pms_calendar_short_info.py create mode 100644 pms_api_rest/services/calendar_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 0b73cc6166..c74370760b 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -1,4 +1,8 @@ -from . import pms_folio_short_info +from . import pms_calendar_search_param +from . import pms_calendar_short_info + from . import pms_folio_search_param +from . import pms_folio_short_info + from . import pms_room_short_info from . import pms_room_search_param diff --git a/pms_api_rest/datamodels/pms_calendar_search_param.py b/pms_api_rest/datamodels/pms_calendar_search_param.py new file mode 100644 index 0000000000..1414c55ecc --- /dev/null +++ b/pms_api_rest/datamodels/pms_calendar_search_param.py @@ -0,0 +1,9 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsCalendarSearchParam(Datamodel): + _name = "pms.calendar.search.param" + date_from = fields.String(required=False, allow_none=True) + date_to = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_calendar_short_info.py b/pms_api_rest/datamodels/pms_calendar_short_info.py new file mode 100644 index 0000000000..54e019a598 --- /dev/null +++ b/pms_api_rest/datamodels/pms_calendar_short_info.py @@ -0,0 +1,12 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsCalendarShortInfo(Datamodel): + _name = "pms.calendar.short.info" + id = fields.Integer(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + roomId = fields.Integer(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + reservationId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room_search_param.py b/pms_api_rest/datamodels/pms_room_search_param.py index 8945237e23..3ef72c8ae7 100644 --- a/pms_api_rest/datamodels/pms_room_search_param.py +++ b/pms_api_rest/datamodels/pms_room_search_param.py @@ -5,6 +5,5 @@ class PmsRoomSearchParam(Datamodel): _name = "pms.room.search.param" - - id = fields.Integer(required=False, allow_none=False) - name = fields.String(required=False, allow_none=False) + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 6329f4b991..67cfea7a84 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -1,2 +1,3 @@ from . import folio_services from . import room_services +from . import calendar_service diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py new file mode 100644 index 0000000000..096d34f704 --- /dev/null +++ b/pms_api_rest/services/calendar_service.py @@ -0,0 +1,49 @@ +from datetime import datetime + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsCalendarService(Component): + _inherit = "base.rest.service" + _name = "pms.calendar.service" + _usage = "calendar" + _collection = "pms.reservation.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.search.param"), + output_param=Datamodel("pms.calendar.short.info", is_list=True), + auth="public", + ) + def get_calendar(self, calendar_search_param): + domain = list() + domain.append(("date", ">=", datetime.fromisoformat(calendar_search_param.date_from))) + domain.append(("date", "<=", datetime.fromisoformat(calendar_search_param.date_to))) + result_lines = [] + PmsCalendarShortInfo = self.env.datamodels["pms.calendar.short.info"] + for line in ( + self.env["pms.reservation.line"] + .sudo() + .search( + domain, + ) + ): + result_lines.append( + PmsCalendarShortInfo( + id=line.id, + roomId=line.room_id.id, + date=datetime.combine(line.date, datetime.min.time()).isoformat(), + partnerId=line.reservation_id.partner_id.id, + reservationId=line.reservation_id, + ) + ) + return result_lines diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index a78c13a1f3..4c6601b3bd 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -72,7 +72,9 @@ def get_folios(self, folio_search_param): "boardService": reservation.board_service_room_id.pms_board_service_id.name if reservation.board_service_room_id else "", - "reservationLines": [] if not reservation_lines else reservation_lines + "reservationLines": [] + if not reservation_lines + else reservation_lines, } ) result_folios.append( @@ -105,7 +107,7 @@ def get_folios(self, folio_search_param): "GET", ) ], - output_param=Datamodel("pms.reservation.short.info", is_list=True), + output_param=Datamodel("pms.folio.short.info", is_list=True), auth="public", ) def get_reservations(self, folio_id): diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py index 787d9d8515..c4a184f421 100644 --- a/pms_api_rest/services/room_services.py +++ b/pms_api_rest/services/room_services.py @@ -1,11 +1,9 @@ -from datetime import datetime - from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component -class PmsFolioService(Component): +class PmsRoomService(Component): _inherit = "base.rest.service" _name = "pms.room.service" _usage = "rooms" @@ -20,8 +18,8 @@ class PmsFolioService(Component): "GET", ) ], - input_param=Datamodel("pms.folio.search.param"), - output_param=Datamodel("pms.folio.short.info", is_list=True), + input_param=Datamodel("pms.room.search.param"), + output_param=Datamodel("pms.room.short.info", is_list=True), auth="public", ) def get_rooms(self, room_search_param): From 920fc1ea07a7f6e51926e17fc88deb03a42147f9 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 6 Oct 2021 09:03:37 +0200 Subject: [PATCH 016/547] [FIX] pms-api-rest: fix calendar filter gte->gt --- pms_api_rest/services/calendar_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index 096d34f704..a9b6fd6117 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -26,7 +26,7 @@ class PmsCalendarService(Component): ) def get_calendar(self, calendar_search_param): domain = list() - domain.append(("date", ">=", datetime.fromisoformat(calendar_search_param.date_from))) + domain.append(("date", ">", datetime.fromisoformat(calendar_search_param.date_from))) domain.append(("date", "<=", datetime.fromisoformat(calendar_search_param.date_to))) result_lines = [] PmsCalendarShortInfo = self.env.datamodels["pms.calendar.short.info"] From 4ed90ffb04f1d61977f1f4cd3a2d6f7fbca7ba24 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 1 Nov 2021 21:05:17 +0100 Subject: [PATCH 017/547] [FIX] pms-pwa: fix calendar filter dates --- pms_api_rest/services/calendar_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index a9b6fd6117..096d34f704 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -26,7 +26,7 @@ class PmsCalendarService(Component): ) def get_calendar(self, calendar_search_param): domain = list() - domain.append(("date", ">", datetime.fromisoformat(calendar_search_param.date_from))) + domain.append(("date", ">=", datetime.fromisoformat(calendar_search_param.date_from))) domain.append(("date", "<=", datetime.fromisoformat(calendar_search_param.date_to))) result_lines = [] PmsCalendarShortInfo = self.env.datamodels["pms.calendar.short.info"] From ff9673a56a6f6cc5f6775479b5b9ff764d4b131f Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 3 Nov 2021 17:41:03 +0100 Subject: [PATCH 018/547] [WIP] pms-api-rest: reservation service --- pms_api_rest/datamodels/__init__.py | 4 ++ .../datamodels/pms_partner_short_info.py | 9 ++++ .../datamodels/pms_reservation_short_info.py | 11 +++++ pms_api_rest/services/__init__.py | 4 ++ pms_api_rest/services/partner_services.py | 42 ++++++++++++++++ pms_api_rest/services/reservation_services.py | 48 +++++++++++++++++++ 6 files changed, 118 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_partner_short_info.py create mode 100644 pms_api_rest/datamodels/pms_reservation_short_info.py create mode 100644 pms_api_rest/services/partner_services.py create mode 100644 pms_api_rest/services/reservation_services.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index c74370760b..393c478044 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -6,3 +6,7 @@ from . import pms_room_short_info from . import pms_room_search_param + +from . import pms_partner_short_info +from . import pms_reservation_short_info + diff --git a/pms_api_rest/datamodels/pms_partner_short_info.py b/pms_api_rest/datamodels/pms_partner_short_info.py new file mode 100644 index 0000000000..0f42d7dbc5 --- /dev/null +++ b/pms_api_rest/datamodels/pms_partner_short_info.py @@ -0,0 +1,9 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPartnerShortInfo(Datamodel): + _name = "pms.partner.short.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_reservation_short_info.py b/pms_api_rest/datamodels/pms_reservation_short_info.py new file mode 100644 index 0000000000..2fa4a3b220 --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation_short_info.py @@ -0,0 +1,11 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsReservationShortInfo(Datamodel): + _name = "pms.reservation.short.info" + id = fields.Integer(required=False, allow_none=True) + price = fields.Float(required=False, allow_none=True) + checkin = fields.String(required=False, allow_none=True) + checkout = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 67cfea7a84..4811e37ab2 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -1,3 +1,7 @@ from . import folio_services from . import room_services from . import calendar_service +from . import partner_services + +from . import reservation_services + diff --git a/pms_api_rest/services/partner_services.py b/pms_api_rest/services/partner_services.py new file mode 100644 index 0000000000..3025ddcc41 --- /dev/null +++ b/pms_api_rest/services/partner_services.py @@ -0,0 +1,42 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsPartnerService(Component): + _inherit = "base.rest.service" + _name = "pms.partner.service" + _usage = "partners" + _collection = "pms.reservation.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.partner.short.info", is_list=True), + auth="public", + ) + def get_partners(self): + domain = [] + result_partners = [] + PmsPartnerShortInfo = self.env.datamodels["pms.partner.short.info"] + for partner in ( + self.env["res.partner"] + .sudo() + .search( + domain, + ) + ): + + result_partners.append( + PmsPartnerShortInfo( + id=partner.id, + name=partner.name, + ) + ) + return result_partners diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py new file mode 100644 index 0000000000..4717a96d23 --- /dev/null +++ b/pms_api_rest/services/reservation_services.py @@ -0,0 +1,48 @@ +from datetime import datetime + +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.reservation.service" + _usage = "reservations" + _collection = "pms.reservation.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="public", + ) + def get_reservations(self): + domain = [] + + result_reservations = [] + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + for reservation in ( + self.env["pms.reservation"] + .sudo() + .search( + domain, + ) + ): + print(type(reservation.checkin)) + result_reservations.append( + + PmsReservationShortInfo( + id=reservation.id, + price=reservation.price_subtotal, + checkin=datetime.combine(reservation.checkin, datetime.min.time()).isoformat(), + checkout=datetime.combine(reservation.checkout, datetime.min.time()).isoformat(), + ) + ) + return result_reservations From 06238c58aa3bf9c97ddce743f0e3d93d7302aaf9 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 24 Nov 2021 12:58:22 +0100 Subject: [PATCH 019/547] [IMP] pms_api_rest: add get_reservation and get_checkin_partners --- pms_api_rest/__manifest__.py | 3 + pms_api_rest/datamodels/__init__.py | 2 +- .../pms_checkin_partner_short_info.py | 18 +++ .../datamodels/pms_reservation_short_info.py | 15 ++ pms_api_rest/services/calendar_service.py | 9 ++ pms_api_rest/services/folio_services.py | 143 +++++++++++++----- 6 files changed, 152 insertions(+), 38 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_checkin_partner_short_info.py diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index bf4ba876f3..582fb9f23b 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -15,5 +15,8 @@ "external_dependencies": { "python": ["jwt", "simplejson", "marshmallow"], }, + "data": [ + "security/ir.model.access.csv", + ], "installable": True, } diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 393c478044..71fd56e93f 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -7,6 +7,6 @@ from . import pms_room_short_info from . import pms_room_search_param -from . import pms_partner_short_info from . import pms_reservation_short_info +from . import pms_checkin_partner_short_info diff --git a/pms_api_rest/datamodels/pms_checkin_partner_short_info.py b/pms_api_rest/datamodels/pms_checkin_partner_short_info.py new file mode 100644 index 0000000000..5afccd3c7c --- /dev/null +++ b/pms_api_rest/datamodels/pms_checkin_partner_short_info.py @@ -0,0 +1,18 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsCheckinPartnerShortInfo(Datamodel): + _name = "pms.checkin.partner.short.info" + id = fields.Integer(required=False, allow_none=True) + # partner = fields.String(required=False, allow_none=True) + reservationId = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + email = fields.String(required=False, allow_none=True) + mobile = fields.String(required=False, allow_none=True) + nationality = fields.String(required=False, allow_none=True) + documentType = fields.String(required=False, allow_none=True) + documentNumber = fields.String(required=False, allow_none=True) + gender = fields.String(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_reservation_short_info.py b/pms_api_rest/datamodels/pms_reservation_short_info.py index 2fa4a3b220..76b0833ddd 100644 --- a/pms_api_rest/datamodels/pms_reservation_short_info.py +++ b/pms_api_rest/datamodels/pms_reservation_short_info.py @@ -6,6 +6,21 @@ class PmsReservationShortInfo(Datamodel): _name = "pms.reservation.short.info" id = fields.Integer(required=False, allow_none=True) +<<<<<<< HEAD price = fields.Float(required=False, allow_none=True) checkin = fields.String(required=False, allow_none=True) checkout = fields.String(required=False, allow_none=True) +======= + partner = fields.String(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + checkin = fields.String(required=False, allow_none=True) + checkout = fields.String(required=False, allow_none=True) + roomTypeId = fields.String(required=False, allow_none=True) + preferredRoomId = fields.String(required=False, allow_none=True) + priceTotal = fields.Float(required=False, allow_none=True) + priceOnlyServices = fields.Float(required=False, allow_none=True) + priceOnlyRoom = fields.Float(required=False, allow_none=True) + pricelist = fields.String(required=False, allow_none=True) + services = fields.List(fields.Dict(required=False, allow_none=True)) + messages = fields.List(fields.Dict(required=False, allow_none=True)) +>>>>>>> d6e6a667... [IMP] pms_api_rest: add get_reservation and get_checkin_partners diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index 096d34f704..140301b76e 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -26,8 +26,17 @@ class PmsCalendarService(Component): ) def get_calendar(self, calendar_search_param): domain = list() +<<<<<<< HEAD domain.append(("date", ">=", datetime.fromisoformat(calendar_search_param.date_from))) domain.append(("date", "<=", datetime.fromisoformat(calendar_search_param.date_to))) +======= + domain.append( + ("date", ">", datetime.fromisoformat(calendar_search_param.date_from)) + ) + domain.append( + ("date", "<=", datetime.fromisoformat(calendar_search_param.date_to)) + ) +>>>>>>> d6e6a667... [IMP] pms_api_rest: add get_reservation and get_checkin_partners result_lines = [] PmsCalendarShortInfo = self.env.datamodels["pms.calendar.short.info"] for line in ( diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 4c6601b3bd..ba307e434f 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -69,12 +69,17 @@ def get_folios(self, folio_search_param): "priceTotal": reservation.price_total, "adults": reservation.adults, "pricelist": reservation.pricelist_id.name, - "boardService": reservation.board_service_room_id.pms_board_service_id.name + "boardService": ( + reservation.board_service_room_id.pms_board_service_id.name + ) if reservation.board_service_room_id else "", "reservationLines": [] if not reservation_lines else reservation_lines, + "folioId": reservation.folio_id.id + if reservation.folio_id + else "", } ) result_folios.append( @@ -102,55 +107,119 @@ def get_folios(self, folio_search_param): [ ( [ - "//reservations", + "//reservations/", ], "GET", ) ], - output_param=Datamodel("pms.folio.short.info", is_list=True), + output_param=Datamodel("pms.reservation.short.info"), auth="public", ) - def get_reservations(self, folio_id): - folio = self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) + def get_reservation(self, folio_id, reservation_id): + reservation = ( + self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) + ) res = [] - if not folio.reservation_ids: + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + if not reservation: pass else: - PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + services = [] + for service in reservation.service_ids: + if service.is_board_service: + services.append( + { + "id": service.id, + "name": service.name, + "quantity": service.product_qty, + "priceTotal": service.price_total, + "priceSubtotal": service.price_subtotal, + "priceTaxes": service.price_tax, + "discount": service.discount, + } + ) + messages = [] + import re - for reservation in folio.reservation_ids: - res.append( - PmsReservationShortInfo( - id=reservation.id, - partner=reservation.partner_id.name, - checkin=str(reservation.checkin), - checkout=str(reservation.checkout), - preferredRoomId=reservation.preferred_room_id.name - if reservation.preferred_room_id + 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 = PmsReservationShortInfo( + id=reservation.id, + partner=reservation.partner_id.name, + checkin=str(reservation.checkin), + checkout=str(reservation.checkout), + preferredRoomId=reservation.preferred_room_id.name + if reservation.preferred_room_id + else "", + roomTypeId=reservation.room_type_id.name + if reservation.room_type_id + else "", + name=reservation.name, + priceTotal=reservation.price_room_services_set, + priceOnlyServices=reservation.price_services + if reservation.price_services + else 0.0, + priceOnlyRoom=reservation.price_total, + pricelist=reservation.pricelist_id.name + if reservation.pricelist_id + else "", + services=services if services else [], + messages=messages, + ) + return res + + @restapi.method( + [ + ( + [ + "//reservations//checkinpartners", + ], + "GET", + ) + ], + output_param=Datamodel("pms.checkin.partner.short.info", is_list=True), + auth="public", + ) + def get_checkin_partners(self, folio_id, reservation_id): + reservation = ( + self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) + ) + checkin_partners = [] + PmsCheckinPartnerShortInfo = self.env.datamodels[ + "pms.checkin.partner.short.info" + ] + if not reservation: + pass + else: + for checkin_partner in reservation.checkin_partner_ids: + checkin_partners.append( + PmsCheckinPartnerShortInfo( + id=checkin_partner.id, + reservationId=checkin_partner.reservation_id.id, + name=checkin_partner.name if checkin_partner.name else "", + email=checkin_partner.email if checkin_partner.email else "", + mobile=checkin_partner.mobile if checkin_partner.mobile else "", + nationality=checkin_partner.nationality_id.name + if checkin_partner.nationality_id else "", - roomTypeId=reservation.room_type_id.name - if reservation.room_type_id + documentType=checkin_partner.document_type.name + if checkin_partner.document_type else "", - name=reservation.name, - partnerRequests=reservation.partner_requests - if reservation.partner_requests + documentNumber=checkin_partner.document_number + if checkin_partner.document_number else "", + gender=checkin_partner.gender if checkin_partner.gender else "", state=dict( - reservation.fields_get(["state"])["state"]["selection"] - )[reservation.state], - priceTotal=reservation.price_total, - adults=reservation.adults, - channelTypeId=reservation.channel_type_id.name - if reservation.channel_type_id - else "", - agencyId=reservation.agency_id.name - if reservation.agency_id - else "", - boardServiceId=reservation.board_service_room_id.pms_board_service_id.name - if reservation.board_service_room_id - else "", - checkinsRatio=reservation.checkins_ratio, - outstanding=reservation.folio_id.pending_amount, + checkin_partner.fields_get(["state"])["state"]["selection"] + )[checkin_partner.state], ) ) - return res + return checkin_partners From 0751fc6d405014a7a8ee72b0386204138e47d5c2 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 24 Nov 2021 16:05:32 +0100 Subject: [PATCH 020/547] [ADD] pms_api_rest: add readme --- pms_api_rest/README.rst | 77 +++++++++++++++++++ pms_api_rest/services/reservation_services.py | 1 - 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 pms_api_rest/README.rst 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/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index 4717a96d23..858504e3c5 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -35,7 +35,6 @@ def get_reservations(self): domain, ) ): - print(type(reservation.checkin)) result_reservations.append( PmsReservationShortInfo( From 3269fc776eb046447f1cc8aa36d898281edf8d16 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 24 Nov 2021 16:33:16 +0100 Subject: [PATCH 021/547] [REF] pms_api_rest: renaming of datamodel files --- pms_api_rest/datamodels/__init__.py | 10 +++++----- ...dar_short_info.py => pms_calendar_info.py} | 4 ++-- ...rt_info.py => pms_checkin_partner_info.py} | 4 ++-- ..._folio_short_info.py => pms_folio_info.py} | 4 ++-- ...room_short_info.py => pms_partner_info.py} | 4 ++-- ..._short_info.py => pms_reservation_info.py} | 4 ++-- ...partner_short_info.py => pms_room_info.py} | 4 ++-- pms_api_rest/services/calendar_service.py | 6 +++--- pms_api_rest/services/folio_services.py | 20 +++++++++---------- pms_api_rest/services/partner_services.py | 6 +++--- pms_api_rest/services/reservation_services.py | 7 +++---- pms_api_rest/services/room_services.py | 6 +++--- 12 files changed, 38 insertions(+), 41 deletions(-) rename pms_api_rest/datamodels/{pms_calendar_short_info.py => pms_calendar_info.py} (83%) rename pms_api_rest/datamodels/{pms_checkin_partner_short_info.py => pms_checkin_partner_info.py} (89%) rename pms_api_rest/datamodels/{pms_folio_short_info.py => pms_folio_info.py} (91%) rename pms_api_rest/datamodels/{pms_room_short_info.py => pms_partner_info.py} (74%) rename pms_api_rest/datamodels/{pms_reservation_short_info.py => pms_reservation_info.py} (93%) rename pms_api_rest/datamodels/{pms_partner_short_info.py => pms_room_info.py} (72%) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 71fd56e93f..d7b92a3844 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -1,12 +1,12 @@ from . import pms_calendar_search_param -from . import pms_calendar_short_info +from . import pms_calendar_info from . import pms_folio_search_param -from . import pms_folio_short_info +from . import pms_folio_info -from . import pms_room_short_info +from . import pms_room_info from . import pms_room_search_param -from . import pms_reservation_short_info +from . import pms_reservation_info -from . import pms_checkin_partner_short_info +from . import pms_checkin_partner_info diff --git a/pms_api_rest/datamodels/pms_calendar_short_info.py b/pms_api_rest/datamodels/pms_calendar_info.py similarity index 83% rename from pms_api_rest/datamodels/pms_calendar_short_info.py rename to pms_api_rest/datamodels/pms_calendar_info.py index 54e019a598..37d95b629d 100644 --- a/pms_api_rest/datamodels/pms_calendar_short_info.py +++ b/pms_api_rest/datamodels/pms_calendar_info.py @@ -3,8 +3,8 @@ from odoo.addons.datamodel.core import Datamodel -class PmsCalendarShortInfo(Datamodel): - _name = "pms.calendar.short.info" +class PmsCalendarInfo(Datamodel): + _name = "pms.calendar.info" id = fields.Integer(required=False, allow_none=True) date = fields.String(required=False, allow_none=True) roomId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_checkin_partner_short_info.py b/pms_api_rest/datamodels/pms_checkin_partner_info.py similarity index 89% rename from pms_api_rest/datamodels/pms_checkin_partner_short_info.py rename to pms_api_rest/datamodels/pms_checkin_partner_info.py index 5afccd3c7c..74df84b0a4 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner_short_info.py +++ b/pms_api_rest/datamodels/pms_checkin_partner_info.py @@ -3,8 +3,8 @@ from odoo.addons.datamodel.core import Datamodel -class PmsCheckinPartnerShortInfo(Datamodel): - _name = "pms.checkin.partner.short.info" +class PmsCheckinPartnerInfo(Datamodel): + _name = "pms.checkin.partner.info" id = fields.Integer(required=False, allow_none=True) # partner = fields.String(required=False, allow_none=True) reservationId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_folio_short_info.py b/pms_api_rest/datamodels/pms_folio_info.py similarity index 91% rename from pms_api_rest/datamodels/pms_folio_short_info.py rename to pms_api_rest/datamodels/pms_folio_info.py index 9c79653c5f..64d40c73e1 100644 --- a/pms_api_rest/datamodels/pms_folio_short_info.py +++ b/pms_api_rest/datamodels/pms_folio_info.py @@ -3,8 +3,8 @@ from odoo.addons.datamodel.core import Datamodel -class PmsFolioShortInfo(Datamodel): - _name = "pms.folio.short.info" +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) diff --git a/pms_api_rest/datamodels/pms_room_short_info.py b/pms_api_rest/datamodels/pms_partner_info.py similarity index 74% rename from pms_api_rest/datamodels/pms_room_short_info.py rename to pms_api_rest/datamodels/pms_partner_info.py index b69b22e535..03f4cb3a5b 100644 --- a/pms_api_rest/datamodels/pms_room_short_info.py +++ b/pms_api_rest/datamodels/pms_partner_info.py @@ -3,7 +3,7 @@ from odoo.addons.datamodel.core import Datamodel -class PmsRoomShortInfo(Datamodel): - _name = "pms.room.short.info" +class PmsPartnerInfo(Datamodel): + _name = "pms.partner.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_reservation_short_info.py b/pms_api_rest/datamodels/pms_reservation_info.py similarity index 93% rename from pms_api_rest/datamodels/pms_reservation_short_info.py rename to pms_api_rest/datamodels/pms_reservation_info.py index 76b0833ddd..c36e7f872d 100644 --- a/pms_api_rest/datamodels/pms_reservation_short_info.py +++ b/pms_api_rest/datamodels/pms_reservation_info.py @@ -3,8 +3,8 @@ from odoo.addons.datamodel.core import Datamodel -class PmsReservationShortInfo(Datamodel): - _name = "pms.reservation.short.info" +class PmsReservationInfo(Datamodel): + _name = "pms.reservation.info" id = fields.Integer(required=False, allow_none=True) <<<<<<< HEAD price = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_partner_short_info.py b/pms_api_rest/datamodels/pms_room_info.py similarity index 72% rename from pms_api_rest/datamodels/pms_partner_short_info.py rename to pms_api_rest/datamodels/pms_room_info.py index 0f42d7dbc5..7bf13106ea 100644 --- a/pms_api_rest/datamodels/pms_partner_short_info.py +++ b/pms_api_rest/datamodels/pms_room_info.py @@ -3,7 +3,7 @@ from odoo.addons.datamodel.core import Datamodel -class PmsPartnerShortInfo(Datamodel): - _name = "pms.partner.short.info" +class PmsRoomInfo(Datamodel): + _name = "pms.room.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index 140301b76e..d1c0eeb0e2 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -21,7 +21,7 @@ class PmsCalendarService(Component): ) ], input_param=Datamodel("pms.calendar.search.param"), - output_param=Datamodel("pms.calendar.short.info", is_list=True), + output_param=Datamodel("pms.calendar.info", is_list=True), auth="public", ) def get_calendar(self, calendar_search_param): @@ -38,7 +38,7 @@ def get_calendar(self, calendar_search_param): ) >>>>>>> d6e6a667... [IMP] pms_api_rest: add get_reservation and get_checkin_partners result_lines = [] - PmsCalendarShortInfo = self.env.datamodels["pms.calendar.short.info"] + PmsCalendarInfo = self.env.datamodels["pms.calendar.info"] for line in ( self.env["pms.reservation.line"] .sudo() @@ -47,7 +47,7 @@ def get_calendar(self, calendar_search_param): ) ): result_lines.append( - PmsCalendarShortInfo( + PmsCalendarInfo( id=line.id, roomId=line.room_id.id, date=datetime.combine(line.date, datetime.min.time()).isoformat(), diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index ba307e434f..bdfeba4b05 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -21,7 +21,7 @@ class PmsFolioService(Component): ) ], input_param=Datamodel("pms.folio.search.param"), - output_param=Datamodel("pms.folio.short.info", is_list=True), + output_param=Datamodel("pms.folio.info", is_list=True), auth="public", ) def get_folios(self, folio_search_param): @@ -31,7 +31,7 @@ def get_folios(self, folio_search_param): if folio_search_param.id: domain.append(("id", "=", folio_search_param.id)) result_folios = [] - PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] + PmsFolioInfo = self.env.datamodels["pms.folio.info"] for folio in ( self.env["pms.folio"] .sudo() @@ -83,7 +83,7 @@ def get_folios(self, folio_search_param): } ) result_folios.append( - PmsFolioShortInfo( + PmsFolioInfo( id=folio.id, name=folio.name, partnerName=folio.partner_name if folio.partner_name else "", @@ -112,7 +112,7 @@ def get_folios(self, folio_search_param): "GET", ) ], - output_param=Datamodel("pms.reservation.short.info"), + output_param=Datamodel("pms.reservation.info"), auth="public", ) def get_reservation(self, folio_id, reservation_id): @@ -120,7 +120,7 @@ def get_reservation(self, folio_id, reservation_id): self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) ) res = [] - PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + PmsReservationInfo = self.env.datamodels["pms.reservation.info"] if not reservation: pass else: @@ -151,7 +151,7 @@ def get_reservation(self, folio_id, reservation_id): "body": re.sub(text, "", message.body), } ) - res = PmsReservationShortInfo( + res = PmsReservationInfo( id=reservation.id, partner=reservation.partner_id.name, checkin=str(reservation.checkin), @@ -185,7 +185,7 @@ def get_reservation(self, folio_id, reservation_id): "GET", ) ], - output_param=Datamodel("pms.checkin.partner.short.info", is_list=True), + output_param=Datamodel("pms.checkin.partner.info", is_list=True), auth="public", ) def get_checkin_partners(self, folio_id, reservation_id): @@ -193,15 +193,13 @@ def get_checkin_partners(self, folio_id, reservation_id): self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) ) checkin_partners = [] - PmsCheckinPartnerShortInfo = self.env.datamodels[ - "pms.checkin.partner.short.info" - ] + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] if not reservation: pass else: for checkin_partner in reservation.checkin_partner_ids: checkin_partners.append( - PmsCheckinPartnerShortInfo( + PmsCheckinPartnerInfo( id=checkin_partner.id, reservationId=checkin_partner.reservation_id.id, name=checkin_partner.name if checkin_partner.name else "", diff --git a/pms_api_rest/services/partner_services.py b/pms_api_rest/services/partner_services.py index 3025ddcc41..5b96977bca 100644 --- a/pms_api_rest/services/partner_services.py +++ b/pms_api_rest/services/partner_services.py @@ -18,13 +18,13 @@ class PmsPartnerService(Component): "GET", ) ], - output_param=Datamodel("pms.partner.short.info", is_list=True), + output_param=Datamodel("pms.partner.info", is_list=True), auth="public", ) def get_partners(self): domain = [] result_partners = [] - PmsPartnerShortInfo = self.env.datamodels["pms.partner.short.info"] + PmsPartnerInfo = self.env.datamodels["pms.partner.info"] for partner in ( self.env["res.partner"] .sudo() @@ -34,7 +34,7 @@ def get_partners(self): ): result_partners.append( - PmsPartnerShortInfo( + PmsPartnerInfo( id=partner.id, name=partner.name, ) diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index 858504e3c5..35954f7134 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -20,14 +20,14 @@ class PmsRoomService(Component): "GET", ) ], - output_param=Datamodel("pms.reservation.short.info", is_list=True), + output_param=Datamodel("pms.reservation.info", is_list=True), auth="public", ) def get_reservations(self): domain = [] result_reservations = [] - PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + PmsReservationInfo = self.env.datamodels["pms.reservation..info"] for reservation in ( self.env["pms.reservation"] .sudo() @@ -36,8 +36,7 @@ def get_reservations(self): ) ): result_reservations.append( - - PmsReservationShortInfo( + PmsReservationInfo( id=reservation.id, price=reservation.price_subtotal, checkin=datetime.combine(reservation.checkin, datetime.min.time()).isoformat(), diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py index c4a184f421..e3b178406f 100644 --- a/pms_api_rest/services/room_services.py +++ b/pms_api_rest/services/room_services.py @@ -19,7 +19,7 @@ class PmsRoomService(Component): ) ], input_param=Datamodel("pms.room.search.param"), - output_param=Datamodel("pms.room.short.info", is_list=True), + output_param=Datamodel("pms.room.info", is_list=True), auth="public", ) def get_rooms(self, room_search_param): @@ -29,7 +29,7 @@ def get_rooms(self, room_search_param): if room_search_param.id: domain.append(("id", "=", room_search_param.id)) result_rooms = [] - PmsRoomShortInfo = self.env.datamodels["pms.room.short.info"] + PmsRoomInfo = self.env.datamodels["pms.room.info"] for room in ( self.env["pms.room"] .sudo() @@ -39,7 +39,7 @@ def get_rooms(self, room_search_param): ): result_rooms.append( - PmsRoomShortInfo( + PmsRoomInfo( id=room.id, name=room.name, ) From a7e30a889b32101bb883ac1dc8aadf46686c69b3 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 24 Nov 2021 17:05:08 +0100 Subject: [PATCH 022/547] [FIX] pms_api_rest: add partner info @ init (datamodels) --- pms_api_rest/datamodels/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index d7b92a3844..c163cb27c9 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -10,3 +10,4 @@ from . import pms_reservation_info from . import pms_checkin_partner_info +from . import pms_partner_info From 6e8e34147386f98f91dc51fde1e40f3cca07aad7 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 25 Nov 2021 12:38:23 +0100 Subject: [PATCH 023/547] [ADD] pms_api_rest: add room type services --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_room_type_info.py | 9 ++++ .../datamodels/pms_room_type_search_param.py | 9 ++++ pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/room_type_services.py | 47 +++++++++++++++++++ 5 files changed, 67 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_room_type_info.py create mode 100644 pms_api_rest/datamodels/pms_room_type_search_param.py create mode 100644 pms_api_rest/services/room_type_services.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index c163cb27c9..fd0bf8e53d 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -5,6 +5,7 @@ from . import pms_folio_info from . import pms_room_info +from . import pms_room_type_info from . import pms_room_search_param from . import pms_reservation_info diff --git a/pms_api_rest/datamodels/pms_room_type_info.py b/pms_api_rest/datamodels/pms_room_type_info.py new file mode 100644 index 0000000000..ce3ad3ea94 --- /dev/null +++ b/pms_api_rest/datamodels/pms_room_type_info.py @@ -0,0 +1,9 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsRoomTypeInfo(Datamodel): + _name = "pms.room.type.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_room_type_search_param.py b/pms_api_rest/datamodels/pms_room_type_search_param.py new file mode 100644 index 0000000000..3ee1780f1a --- /dev/null +++ b/pms_api_rest/datamodels/pms_room_type_search_param.py @@ -0,0 +1,9 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsRoomTypeSearchParam(Datamodel): + _name = "pms.room.type.search.param" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 4811e37ab2..ca589e26ce 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -1,5 +1,6 @@ from . import folio_services from . import room_services +from . import room_type_services from . import calendar_service from . import partner_services diff --git a/pms_api_rest/services/room_type_services.py b/pms_api_rest/services/room_type_services.py new file mode 100644 index 0000000000..b628da6256 --- /dev/null +++ b/pms_api_rest/services/room_type_services.py @@ -0,0 +1,47 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsRoomTypeService(Component): + _inherit = "base.rest.service" + _name = "pms.room.type.service" + _usage = "room-types" + _collection = "pms.reservation.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.room.search.param"), + output_param=Datamodel("pms.room.info", is_list=True), + auth="public", + ) + def get_room_types(self, room_type_search_param): + domain = [] + if room_type_search_param.name: + domain.append(("name", "like", room_type_search_param.name)) + if room_type_search_param.id: + domain.append(("id", "=", room_type_search_param.id)) + result_rooms = [] + PmsRoomTypeInfo = self.env.datamodels["pms.room.type.info"] + for room in ( + self.env["pms.room.type"] + .sudo() + .search( + domain, + ) + ): + + result_rooms.append( + PmsRoomTypeInfo( + id=room.id, + name=room.name, + ) + ) + return result_rooms From 40fc355d7b4974da29ce0b761ef32733bc489b31 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 30 Nov 2021 11:14:05 +0100 Subject: [PATCH 024/547] [IMP] pms-api-rest: add room_type_id @ room service --- pms_api_rest/datamodels/pms_room_info.py | 1 + pms_api_rest/services/room_services.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_room_info.py b/pms_api_rest/datamodels/pms_room_info.py index 7bf13106ea..5a5d5a4650 100644 --- a/pms_api_rest/datamodels/pms_room_info.py +++ b/pms_api_rest/datamodels/pms_room_info.py @@ -7,3 +7,4 @@ 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) diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py index e3b178406f..1727528a18 100644 --- a/pms_api_rest/services/room_services.py +++ b/pms_api_rest/services/room_services.py @@ -42,6 +42,7 @@ def get_rooms(self, room_search_param): PmsRoomInfo( id=room.id, name=room.name, + roomTypeId=room.room_type_id, ) ) return result_rooms From bb1d6a6fbecd0abc33c3fa583edae925e1acd49c Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 10 Dec 2021 17:13:21 +0100 Subject: [PATCH 025/547] [IMP] pms-api-rest: swap reservation lines --- pms_api_rest/datamodels/__init__.py | 2 + .../datamodels/pms_calendar_swap_info.py | 11 +++++ pms_api_rest/services/calendar_service.py | 46 +++++++++++++++++-- 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_calendar_swap_info.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index fd0bf8e53d..155aaec3e2 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -12,3 +12,5 @@ from . import pms_checkin_partner_info from . import pms_partner_info + +from . import pms_calendar_swap_info diff --git a/pms_api_rest/datamodels/pms_calendar_swap_info.py b/pms_api_rest/datamodels/pms_calendar_swap_info.py new file mode 100644 index 0000000000..ee9f2a0bb0 --- /dev/null +++ b/pms_api_rest/datamodels/pms_calendar_swap_info.py @@ -0,0 +1,11 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsCalendarSwapInfo(Datamodel): + _name = "pms.calendar.swap.info" + swapFrom = fields.String(required=True, allow_none=False) + swapTo = fields.String(required=True, allow_none=False) + roomIdA = fields.Integer(required=True, allow_none=False) + roomIdB = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index d1c0eeb0e2..6132f87562 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -26,17 +26,12 @@ class PmsCalendarService(Component): ) def get_calendar(self, calendar_search_param): domain = list() -<<<<<<< HEAD - domain.append(("date", ">=", datetime.fromisoformat(calendar_search_param.date_from))) - domain.append(("date", "<=", datetime.fromisoformat(calendar_search_param.date_to))) -======= domain.append( ("date", ">", datetime.fromisoformat(calendar_search_param.date_from)) ) domain.append( ("date", "<=", datetime.fromisoformat(calendar_search_param.date_to)) ) ->>>>>>> d6e6a667... [IMP] pms_api_rest: add get_reservation and get_checkin_partners result_lines = [] PmsCalendarInfo = self.env.datamodels["pms.calendar.info"] for line in ( @@ -56,3 +51,44 @@ def get_calendar(self, calendar_search_param): ) ) return result_lines + + @restapi.method( + [ + ( + [ + "/swap", + ], + "POST", + ) + ], + input_param=Datamodel("pms.calendar.swap.info", is_list=False), + auth="public", + ) + def swap_reservation_slices(self, swap_info): + room_id_a = swap_info.roomIdA + room_id_b = swap_info.roomIdB + + lines_room_a = self.env["pms.reservation.line"].search( + [ + ("room_id", "=", room_id_a), + ("date", ">=", swap_info.swapFrom), + ("date", "<=", swap_info.swapTo), + ] + ) + + lines_room_b = self.env["pms.reservation.line"].search( + [ + ("room_id", "=", room_id_b), + ("date", ">=", swap_info.swapFrom), + ("date", "<=", swap_info.swapTo), + ] + ) + lines_room_a.occupies_availability = False + lines_room_b.occupies_availability = False + lines_room_a.flush() + lines_room_b.flush() + lines_room_a.room_id = room_id_b + lines_room_b.room_id = room_id_a + + lines_room_a._compute_occupies_availability() + lines_room_b._compute_occupies_availability() From 89619352ee7187eded63a8c7d14418a8afede987 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 16 Dec 2021 09:53:43 +0100 Subject: [PATCH 026/547] [IMP] pms_api_rest: controller to modify reservation lines (room_id, & dates) --- pms_api_rest/datamodels/__init__.py | 1 + .../datamodels/pms_calendar_changes.py | 8 +++ pms_api_rest/services/reservation_services.py | 50 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_calendar_changes.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 155aaec3e2..69d305d63c 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -14,3 +14,4 @@ from . import pms_partner_info from . import pms_calendar_swap_info +from . import pms_calendar_changes diff --git a/pms_api_rest/datamodels/pms_calendar_changes.py b/pms_api_rest/datamodels/pms_calendar_changes.py new file mode 100644 index 0000000000..77e3e70747 --- /dev/null +++ b/pms_api_rest/datamodels/pms_calendar_changes.py @@ -0,0 +1,8 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsCalendarChanges(Datamodel): + _name = "pms.calendar.changes" + reservationLinesChanges = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index 35954f7134..f400ee8f92 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -44,3 +44,53 @@ def get_reservations(self): ) ) return result_reservations + + @restapi.method( + [ + ( + [ + "/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.calendar.changes", is_list=False), + auth="public", + ) + def move_reservation_line(self, reservation_id, reservation_lines_changes): + + # 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"] From 7bf3088ed53e32ca5617d2335286d5deea5da7a0 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Sun, 26 Dec 2021 12:06:28 +0100 Subject: [PATCH 027/547] [IMP] pms-api-rest: adapt jwt auth to oca/rest-framework's lasts udpates --- pms_api_rest/__init__.py | 1 - pms_api_rest/__manifest__.py | 3 +- pms_api_rest/controllers/__init__.py | 1 - pms_api_rest/controllers/jwt_controller.py | 110 ----------------- pms_api_rest/controllers/pms_rest.py | 17 +-- pms_api_rest/data/auth_jwt_validator.xml | 14 +++ pms_api_rest/lib_jwt/jwt_http.py | 131 --------------------- pms_api_rest/lib_jwt/util.py | 85 ------------- pms_api_rest/lib_jwt/validator.py | 107 ----------------- pms_api_rest/models/__init__.py | 2 - pms_api_rest/models/jwt_access_token.py | 29 ----- pms_api_rest/models/res_users.py | 43 ------- pms_api_rest/services/partner_services.py | 1 - 13 files changed, 17 insertions(+), 527 deletions(-) delete mode 100644 pms_api_rest/controllers/jwt_controller.py create mode 100644 pms_api_rest/data/auth_jwt_validator.xml delete mode 100644 pms_api_rest/lib_jwt/jwt_http.py delete mode 100644 pms_api_rest/lib_jwt/util.py delete mode 100644 pms_api_rest/lib_jwt/validator.py delete mode 100644 pms_api_rest/models/__init__.py delete mode 100644 pms_api_rest/models/jwt_access_token.py delete mode 100644 pms_api_rest/models/res_users.py diff --git a/pms_api_rest/__init__.py b/pms_api_rest/__init__.py index e14ece83df..5b9cd7bd00 100644 --- a/pms_api_rest/__init__.py +++ b/pms_api_rest/__init__.py @@ -1,4 +1,3 @@ from . import controllers from . import datamodels -from . import models from . import services diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 582fb9f23b..588a57b711 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -11,12 +11,13 @@ "base_rest_datamodel", "web", "auth_signup", + "auth_jwt_login", ], "external_dependencies": { "python": ["jwt", "simplejson", "marshmallow"], }, "data": [ - "security/ir.model.access.csv", + "security/ir.model.access.csv", "data/auth_jwt_validator.xml" ], "installable": True, } diff --git a/pms_api_rest/controllers/__init__.py b/pms_api_rest/controllers/__init__.py index edc7f8a32f..5e366b40cb 100644 --- a/pms_api_rest/controllers/__init__.py +++ b/pms_api_rest/controllers/__init__.py @@ -1,2 +1 @@ -from . import jwt_controller from . import pms_rest diff --git a/pms_api_rest/controllers/jwt_controller.py b/pms_api_rest/controllers/jwt_controller.py deleted file mode 100644 index d30237b137..0000000000 --- a/pms_api_rest/controllers/jwt_controller.py +++ /dev/null @@ -1,110 +0,0 @@ -import logging - -import werkzeug - -from odoo import http -from odoo.http import request - -from odoo.addons.auth_signup.models.res_users import SignupError - -from ..lib_jwt.jwt_http import jwt_http -from ..lib_jwt.validator import validator - -_logger = logging.getLogger(__name__) - -SENSITIVE_FIELDS = [ - "password", - "password_crypt", - "new_password", - "create_uid", - "write_uid", -] - - -class JwtController(http.Controller): - # test route - @http.route("/api/info", auth="public", csrf=False, cors="*") - def index(self, **kw): - return "Hello, world" - - @http.route( - "/api/login", type="http", auth="public", csrf=False, cors="*", methods=["POST"] - ) - def login(self, email, password, **kw): - return jwt_http.do_login(email, password) - - @http.route("/api/me", type="http", auth="public", csrf=False, cors="*") - def me(self, **kw): - http_method, body, headers, token = jwt_http.parse_request() - result = validator.verify_token(token) - if not result["status"]: - return jwt_http.errcode(code=result["code"], message=result["message"]) - - return jwt_http.response(request.env.user.to_dict(True)) - - @http.route("/api/logout", type="http", auth="public", csrf=False, cors="*") - def logout(self, **kw): - http_method, body, headers, token = jwt_http.parse_request() - result = validator.verify_token(token) - if not result["status"]: - return jwt_http.errcode(code=result["code"], message=result["message"]) - - jwt_http.do_logout(token) - return jwt_http.response() - - @http.route( - "/api/register", - type="http", - auth="public", - csrf=False, - cors="*", - methods=["POST"], - ) - def register(self, email=None, name=None, password=None, **kw): - if not validator.is_valid_email(email): - return jwt_http.errcode(code=400, message="Invalid email address") - if not name: - return jwt_http.errcode(code=400, message="Name cannot be empty") - if not password: - return jwt_http.errcode(code=400, message="Password cannot be empty") - - # sign up - try: - self._signup_with_values(login=email, name=name, password=password) - except AttributeError: - return jwt_http.errcode(code=501, message="Signup is disabled") - except (SignupError, AssertionError) as e: - if request.env["res.users"].sudo().search([("login", "=", email)]): - return jwt_http.errcode( - code=400, message="Email address already exists" - ) - else: - _logger.error("%s", e) - return jwt_http.response_500() - except Exception as e: - _logger.error(str(e)) - return jwt_http.response_500() - # log the user in - return jwt_http.do_login(email, password) - - def _signup_with_values(self, **values): - request.env["res.users"].sudo().signup(values, None) - request.env.cr.commit() # as authenticate will use its - # own cursor we need to commit the current transaction - self.signup_email(values) - - def signup_email(self, values): - user_sudo = ( - request.env["res.users"] - .sudo() - .search([("login", "=", values.get("login"))]) - ) - template = request.env.ref( - "auth_signup.mail_template_user_signup_account_created", - raise_if_not_found=False, - ) - if user_sudo and template: - template.sudo().with_context( - lang=user_sudo.lang, - auth_login=werkzeug.url_encode({"auth_login": user_sudo.email}), - ).send_mail(user_sudo.id, force_send=True) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index 1669215d05..f410b058ce 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -1,22 +1,7 @@ from odoo.addons.base_rest.controllers import main -from ..lib_jwt.jwt_http import jwt_http -from ..lib_jwt.validator import validator - class BaseRestDemoPublicApiController(main.RestController): _root_path = "/api/" _collection_name = "pms.reservation.service" - _default_auth = "public" - - # RestController OVERRIDE method - def _process_method(self, service_name, method_name, *args, params=None): - - http_method, body, headers, token = jwt_http.parse_request() - result = validator.verify_token(token) - if not result["status"]: - return jwt_http.errcode(code=result["code"], message=result["message"]) - else: - return super(BaseRestDemoPublicApiController, self)._process_method( - service_name, method_name, *args, params=params - ) + _default_auth = "jwt_api_pms" 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/lib_jwt/jwt_http.py b/pms_api_rest/lib_jwt/jwt_http.py deleted file mode 100644 index e96d144b38..0000000000 --- a/pms_api_rest/lib_jwt/jwt_http.py +++ /dev/null @@ -1,131 +0,0 @@ -import datetime - -import simplejson as json - -from odoo import http -from odoo.exceptions import AccessDenied -from odoo.http import Response, request - -from .validator import validator - -return_fields = ["id", "login", "name", "company_id"] - - -class JwtHttp: - def get_state(self): - return {"d": request.session.db} - - def parse_request(self): - http_method = request.httprequest.method - try: - body = http.request.params - except Exception: - body = {} - - headers = dict(list(request.httprequest.headers.items())) - if "wsgi.input" in headers: - del headers["wsgi.input"] - if "wsgi.errors" in headers: - del headers["wsgi.errors"] - if "HTTP_AUTHORIZATION" in headers: - headers["Authorization"] = headers["HTTP_AUTHORIZATION"] - - # extract token - token = "" - if "Authorization" in headers: - try: - # Bearer token_string - token = headers["Authorization"].split(" ")[1] - except Exception: - pass - - return http_method, body, headers, token - - def date2str(self, d, f="%Y-%m-%d %H:%M:%S"): - """ - Convert datetime to string - :param self: - :param d: datetime object - :param f='%Y-%m-%d%H:%M:%S': string format - """ - try: - s = d.strftime(f) - except Exception: - s = None - - return s - - def response(self, success=True, message=None, data=None, code=200): - """ - Create a HTTP Response for controller - :param success=True indicate this response is successful or not - :param message=None message string - :param data=None data to return - :param code=200 http status code - """ - - payload = json.dumps( - { - "success": success, - "message": message, - "data": data, - } - ) - - return Response( - payload, - status=code, - headers=[ - ("Content-Type", "application/json"), - ], - ) - - def response_500(self, message="Internal Server Error", data=None): - return self.response(success=False, message=message, data=data, code=500) - - def response_401(self, message="401 Unauthorized", data=None): - return self.response(success=False, message=message, data=data, code=401) - - def response_404(self, message="404 Not Found", data=None): - return self.response(success=False, message=message, data=data, code=404) - - def response_403(self, message="403 Forbidden", data=None): - return self.response(success=False, message=message, data=data, code=403) - - def errcode(self, code, message=None): - return self.response(success=False, code=code, message=message) - - def do_login(self, login, password): - # get current db - state = self.get_state() - try: - uid = request.session.authenticate(state["d"], login, password) - except AccessDenied: - return self.response_401() - if not uid: - return self.response_401() - - # login success, generate token - user = request.env.user.read(return_fields)[0] - exp = datetime.datetime.utcnow() + datetime.timedelta(minutes=30000) - token = validator.create_token(user, exp) - - return self.response( - data={"user": user, "exp": json.dumps(exp.isoformat()), "token": token} - ) - - def do_logout(self, token): - request.session.logout() - request.env["jwt_provider.access_token"].sudo().search( - [("token", "=", token)] - ).unlink() - return self.response() - - def cleanup(self): - # Clean up things after success request - # use logout here to make request as stateless as possible - request.session.logout() - return self.response() - - -jwt_http = JwtHttp() diff --git a/pms_api_rest/lib_jwt/util.py b/pms_api_rest/lib_jwt/util.py deleted file mode 100644 index 8a8d465d4e..0000000000 --- a/pms_api_rest/lib_jwt/util.py +++ /dev/null @@ -1,85 +0,0 @@ -import logging -import os -import random -import string - -from dateutil.parser import parse - -_logger = logging.getLogger(__name__) - - -class Util: - addons_path = os.path.join(os.path.dirname(os.path.abspath(__file__))) - - def __init__(self): - self.addons_path = self.addons_path.replace("jwt_provider", "") - - def generate_verification_code(self, length=8): - return "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(length) - ) - - def toDate(self, pgTimeStr): - return parse(pgTimeStr) - - def path(self, *paths): - """Make a path""" - return os.path.join(self.addons_path, *paths) - - def add_branch(self, tree, vector, value): - """ - Given a dict, a vector, and a value, insert the value into the dict - at the tree leaf specified by the vector. Recursive! - - Params: - data (dict): The data structure to insert the vector into. - vector (list): A list of values representing the path to the leaf node. - value (object): The object to be inserted at the leaf - - Example 1: - tree = {'a': 'apple'} - vector = ['b', 'c', 'd'] - value = 'dog' - - tree = add_branch(tree, vector, value) - - Returns: - tree = { 'a': 'apple', 'b': { 'c': {'d': 'dog'}}} - - Example 2: - vector2 = ['b', 'c', 'e'] - value2 = 'egg' - - tree = add_branch(tree, vector2, value2) - - Returns: - tree = { 'a': 'apple', 'b': { 'c': {'d': 'dog', 'e': 'egg'}}} - - Returns: - dict: The dict with the value placed at the path specified. - - Algorithm: - If we're at the leaf, add it as key/value to the tree - Else: If the subtree doesn't exist, create it. - Recurse with the subtree and the left shifted vector. - Return the tree. - - """ - key = vector[0] - tree[key] = ( - value - if len(vector) == 1 - else self.add_branch(tree[key] if key in tree else {}, vector[1:], value) - ) - return tree - - def create_dict(self, d): - res = {} - for k, v in d.items(): - ar = k.split(".") - filter(None, ar) - self.add_branch(res, ar, v) - return res - - -util = Util() diff --git a/pms_api_rest/lib_jwt/validator.py b/pms_api_rest/lib_jwt/validator.py deleted file mode 100644 index b838e3d5a1..0000000000 --- a/pms_api_rest/lib_jwt/validator.py +++ /dev/null @@ -1,107 +0,0 @@ -import datetime -import logging -import re -import traceback - -import jwt -from jwt import InvalidSignatureError - -from odoo.http import request -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT - -_logger = logging.getLogger(__name__) - -regex = ( - r"^[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9]" -) -regex += r"(?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" - - -class Validator: - def is_valid_email(self, email): - return re.search(regex, email) - - def key(self): - # TODO: change this key before production (build an UI Form to maintain - # (in form company?) - return "CHANGE THIS KEY" - - def create_token(self, user, exp): - try: - payload = { - "exp": exp, - "iat": datetime.datetime.utcnow(), - "sub": user["id"], - "lgn": user["login"], - } - token = jwt.encode(payload, self.key(), algorithm="HS256") - - self.save_token(token, user["id"], exp) - return token - except Exception as ex: - _logger.error(ex) - raise - - def save_token(self, token, uid, exp): - request.env["jwt_provider.access_token"].sudo().create( - { - "user_id": uid, - "expires": exp.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - "token": token, - } - ) - - def verify(self, token): - record = ( - request.env["jwt_provider.access_token"] - .sudo() - .search([("token", "=", token)]) - ) - - if len(record) != 1: - _logger.info("not found %s" % token) - return False - - if record.is_expired: - return False - - return record.user_id - - def verify_token(self, token): - try: - result = { - "status": False, - "message": None, - } - - if not self.verify(token): - result["message"] = "Token invalid or expired" - result["code"] = 498 - _logger.info("11111") - return result - - payload = jwt.decode(token, self.key(), algorithms=["HS256"]) - uid = request.session.authenticate( - request.session.db, login=payload["lgn"], password=token - ) - if not uid: - result["message"] = "Token invalid or expired" - result["code"] = 498 - _logger.info("2222") - return result - - result["status"] = True - return result - except ( - jwt.ExpiredSignatureError, - jwt.InvalidTokenError, - InvalidSignatureError, - Exception, - ): - result["code"] = 498 - result["message"] = "Token invalid or expired" - _logger.error(traceback.format_exc()) - return result - - -validator = Validator() diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py deleted file mode 100644 index eb19928999..0000000000 --- a/pms_api_rest/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import res_users -from . import jwt_access_token diff --git a/pms_api_rest/models/jwt_access_token.py b/pms_api_rest/models/jwt_access_token.py deleted file mode 100644 index 4350e359e6..0000000000 --- a/pms_api_rest/models/jwt_access_token.py +++ /dev/null @@ -1,29 +0,0 @@ -from datetime import datetime - -from odoo import api, fields, models - - -class JwtAccessToken(models.Model): - _name = "jwt_provider.access_token" - _description = "Store user access token for one-time-login" - - token = fields.Char("Access Token", required=True) - user_id = fields.Many2one( - comodel_name="res.users", - string="User", - required=True, - ondelete="cascade", - ) - expires = fields.Datetime( - "Expires", - required=True, - ) - - is_expired = fields.Boolean( - compute="_compute_is_expired", - ) - - @api.depends("expires") - def _compute_is_expired(self): - for token in self: - token.is_expired = datetime.now() > token.expires diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py deleted file mode 100644 index b7203be401..0000000000 --- a/pms_api_rest/models/res_users.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging - -from odoo import api, fields, models -from odoo.exceptions import AccessDenied - -from ..lib_jwt.validator import validator - -_logger = logging.getLogger(__name__) - - -class ResUsers(models.Model): - _inherit = "res.users" - - access_token_ids = fields.One2many( - string="Access Tokens", - comodel_name="jwt_provider.access_token", - inverse_name="user_id", - ) - - @classmethod - def _login(cls, db, login, password, user_agent_env): - user_id = super(ResUsers, cls)._login(db, login, password, user_agent_env) - if user_id: - return user_id - uid = validator.verify(password) - _logger.info(uid) - return uid - - @api.model - def _check_credentials(self, password, user_agent_env): - try: - super(ResUsers, self)._check_credentials(password, user_agent_env) - except AccessDenied: - if not validator.verify(password): - raise - - def to_dict(self, single=False): - res = [] - for u in self: - d = u.read(["email", "name", "company_id"])[0] - res.append(d) - - return res[0] if single else res diff --git a/pms_api_rest/services/partner_services.py b/pms_api_rest/services/partner_services.py index 5b96977bca..1b7a45cca0 100644 --- a/pms_api_rest/services/partner_services.py +++ b/pms_api_rest/services/partner_services.py @@ -19,7 +19,6 @@ class PmsPartnerService(Component): ) ], output_param=Datamodel("pms.partner.info", is_list=True), - auth="public", ) def get_partners(self): domain = [] From 7b57b9306b4e414116f59e811492ba19d2a8b3d7 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 27 Dec 2021 09:44:23 +0100 Subject: [PATCH 028/547] [IMP] pms_api_rest: add payments in folio and property service --- pms_api_rest/datamodels/__init__.py | 5 + .../datamodels/pms_account_journal_info.py | 10 ++ pms_api_rest/datamodels/pms_folio_info.py | 2 + pms_api_rest/datamodels/pms_payment_info.py | 12 ++ pms_api_rest/datamodels/pms_property_info.py | 10 ++ .../datamodels/pms_property_search_param.py | 10 ++ pms_api_rest/services/__init__.py | 2 +- pms_api_rest/services/folio_services.py | 54 ++++++++ pms_api_rest/services/property_services.py | 115 ++++++++++++++++++ 9 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 pms_api_rest/datamodels/pms_account_journal_info.py create mode 100644 pms_api_rest/datamodels/pms_payment_info.py create mode 100644 pms_api_rest/datamodels/pms_property_info.py create mode 100644 pms_api_rest/datamodels/pms_property_search_param.py create mode 100644 pms_api_rest/services/property_services.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 69d305d63c..3f34fd5dff 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -15,3 +15,8 @@ from . import pms_calendar_swap_info from . import pms_calendar_changes + +from . import pms_property_info +from . import pms_property_search_param +from . import pms_account_journal_info +from . import pms_payment_info diff --git a/pms_api_rest/datamodels/pms_account_journal_info.py b/pms_api_rest/datamodels/pms_account_journal_info.py new file mode 100644 index 0000000000..25e2f76833 --- /dev/null +++ b/pms_api_rest/datamodels/pms_account_journal_info.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAccountJournalInfo(Datamodel): + _name = "pms.account.journal.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + allowed_pms_payments = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_folio_info.py b/pms_api_rest/datamodels/pms_folio_info.py index 64d40c73e1..54fe504b10 100644 --- a/pms_api_rest/datamodels/pms_folio_info.py +++ b/pms_api_rest/datamodels/pms_folio_info.py @@ -16,3 +16,5 @@ class PmsFolioInfo(Datamodel): pendingAmount = fields.Float(required=False, allow_none=True) reservations = fields.List(fields.Dict(required=False, allow_none=True)) salesPerson = fields.String(required=False, allow_none=True) + paymentState = fields.String(required=False, allow_none=True) + propertyId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_payment_info.py b/pms_api_rest/datamodels/pms_payment_info.py new file mode 100644 index 0000000000..63d34602bd --- /dev/null +++ b/pms_api_rest/datamodels/pms_payment_info.py @@ -0,0 +1,12 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPaymentInfo(Datamodel): + _name = "pms.payment.info" + id = fields.Integer(required=False, allow_none=True) + amount = fields.Float(required=False, allow_none=True) + journalId = fields.Integer(required=False, allow_none=True) + journalName = fields.String(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_property_info.py b/pms_api_rest/datamodels/pms_property_info.py new file mode 100644 index 0000000000..79db922407 --- /dev/null +++ b/pms_api_rest/datamodels/pms_property_info.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPropertyInfo(Datamodel): + _name = "pms.property.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + company = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_property_search_param.py b/pms_api_rest/datamodels/pms_property_search_param.py new file mode 100644 index 0000000000..c8a1f81b46 --- /dev/null +++ b/pms_api_rest/datamodels/pms_property_search_param.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPropertySearchParam(Datamodel): + _name = "pms.property.search.param" + + id = fields.Integer(required=False, allow_none=False) + name = fields.String(required=False, allow_none=False) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index ca589e26ce..87c212ea52 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -5,4 +5,4 @@ from . import partner_services from . import reservation_services - +from . import property_services diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index bdfeba4b05..1918bf0784 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -99,6 +99,10 @@ def get_folios(self, folio_search_param): pendingAmount=folio.pending_amount, reservations=[] if not reservations else reservations, salesPerson=folio.user_id.name if folio.user_id else "", + paymentState=dict(folio.fields_get(["payment_state"])["payment_state"]["selection"])[ + folio.payment_state + ] if folio.payment_state else "", + propertyId=folio.pms_property_id, ) ) return result_folios @@ -221,3 +225,53 @@ def get_checkin_partners(self, folio_id, reservation_id): ) ) return checkin_partners + + @restapi.method( + [ + ( + [ + "//payments", + ], + "GET", + ) + ], + output_param=Datamodel("pms.payment.info", is_list=True), + auth="public", + ) + def get_folio_payments(self, folio_id): + folio = ( + self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) + ) + payments = [] + PmsPaymentInfo = self.env.datamodels["pms.payment.info"] + if not folio: + pass + else: + if folio.payment_state == "not_paid": + pass + # si el folio está sin pagar no tendrá ningún pago o envíar []? + else: + if folio.statement_line_ids: + for payment in folio.statement_line_ids: + payments.append( + PmsPaymentInfo( + id=payment.id, + amount=payment.amount, + journalId=payment.journal_id, + journalName=payment.journal_id.name, + date=str(payment.date), + ) + ) + if folio.payment_ids: + if folio.payment_ids: + for payment in folio.payment_ids: + payments.append( + PmsPaymentInfo( + id=payment.id, + amount=payment.amount, + journalId=payment.journal_id, + journalName=payment.journal_id.name, + date=str(payment.date), + ) + ) + return payments diff --git a/pms_api_rest/services/property_services.py b/pms_api_rest/services/property_services.py new file mode 100644 index 0000000000..926cdd7949 --- /dev/null +++ b/pms_api_rest/services/property_services.py @@ -0,0 +1,115 @@ +from datetime import datetime + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsPropertyComponent(Component): + _inherit = "base.rest.service" + _name = "pms.property.service" + _usage = "properties" + _collection = "pms.reservation.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.property.search.param"), + output_param=Datamodel("pms.property.info", is_list=True), + auth="public", + ) + def get_properties(self,property_search_param): + domain = [] + if property_search_param.name: + domain.append(("name", "like", property_search_param.name)) + if property_search_param.id: + domain.append(("id", "=", property_search_param.id)) + result_properties = [] + PmsPropertyInfo = self.env.datamodels["pms.property.info"] + for prop in ( + self.env["pms.property"] + .sudo() + .search( + domain, + ) + ): + result_properties.append( + PmsPropertyInfo( + id=prop.id, + name=prop.name, + company=prop.company_id.name, + ) + ) + return result_properties + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.property.info"), + auth="public", + ) + def get_property(self, property_id): + pms_property = ( + self.env["pms.property"].sudo().search([("id", "=", property_id)]) + ) + res = [] + PmsPropertyInfo = self.env.datamodels["pms.property.info"] + if not pms_property: + pass + else: + res = PmsPropertyInfo( + id=pms_property.id, + name=pms_property.name, + company=pms_property.company_id.name, + ) + + return res + + @restapi.method( + [ + ( + [ + "//paymentmethods", + ], + "GET", + ) + ], + output_param=Datamodel("pms.account.journal.info",is_list=True), + auth="public", + ) + def get_method_payments_property(self, property_id): + + property = ( + self.env["pms.property"].sudo().search([("id", "=", property_id)]) + ) + PmsAccountJournalInfo = self.env.datamodels["pms.account.journal.info"] + res = [] + if not property: + pass + else: + for method in property._get_payment_methods( + automatic_included=True + ): + payment_method = ( + self.env["account.journal"].sudo().search([("id", "=", method.id)]) + ) + res.append( + PmsAccountJournalInfo( + id=payment_method.id, + name=payment_method.name, + allowed_pms_payments=payment_method.allowed_pms_payments, + ) + ) + return res From d6de2206f41c748eb5c7da165756a5583759bb2b Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 27 Dec 2021 11:31:35 +0100 Subject: [PATCH 029/547] [REF] pms-api-rest: add controller 4 login --- pms_api_rest/__manifest__.py | 4 +- pms_api_rest/controllers/pms_rest.py | 10 +- pms_api_rest/datamodels/__init__.py | 2 + pms_api_rest/datamodels/user_input.py | 9 ++ pms_api_rest/datamodels/user_output.py | 10 ++ pms_api_rest/security/ir.model.access.csv | 2 - pms_api_rest/services/__init__.py | 4 + pms_api_rest/services/calendar_service.py | 2 +- pms_api_rest/services/folio_services.py | 2 +- pms_api_rest/services/login_service.py | 116 ++++++++++++++++++ pms_api_rest/services/partner_services.py | 2 +- pms_api_rest/services/reservation_services.py | 2 +- pms_api_rest/services/room_services.py | 2 +- pms_api_rest/services/room_type_services.py | 2 +- 14 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 pms_api_rest/datamodels/user_input.py create mode 100644 pms_api_rest/datamodels/user_output.py delete mode 100644 pms_api_rest/security/ir.model.access.csv create mode 100644 pms_api_rest/services/login_service.py diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 588a57b711..2529e90b0e 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -16,8 +16,6 @@ "external_dependencies": { "python": ["jwt", "simplejson", "marshmallow"], }, - "data": [ - "security/ir.model.access.csv", "data/auth_jwt_validator.xml" - ], + "data": ["data/auth_jwt_validator.xml"], "installable": True, } diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index f410b058ce..5a738e502a 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -1,7 +1,13 @@ from odoo.addons.base_rest.controllers import main -class BaseRestDemoPublicApiController(main.RestController): +class BaseRestPrivateApiController(main.RestController): _root_path = "/api/" - _collection_name = "pms.reservation.service" + _collection_name = "pms.private.services" _default_auth = "jwt_api_pms" + + +class BaseRestPublicApiController(main.RestController): + _root_path = "/auth/" + _collection_name = "pms.public.services" + _default_auth = "public" diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 3f34fd5dff..248ce06e53 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -20,3 +20,5 @@ from . import pms_property_search_param from . import pms_account_journal_info from . import pms_payment_info +from . import user_input +from . import user_output diff --git a/pms_api_rest/datamodels/user_input.py b/pms_api_rest/datamodels/user_input.py new file mode 100644 index 0000000000..f370c2a609 --- /dev/null +++ b/pms_api_rest/datamodels/user_input.py @@ -0,0 +1,9 @@ +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) diff --git a/pms_api_rest/datamodels/user_output.py b/pms_api_rest/datamodels/user_output.py new file mode 100644 index 0000000000..ee17fe5cac --- /dev/null +++ b/pms_api_rest/datamodels/user_output.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsApiRestUserOutput(Datamodel): + _name = "pms.api.rest.user.output" + # user = fields.String(required=False, allow_none=True) + # exp = fields.String(required=False, allow_none=True) + token = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/security/ir.model.access.csv b/pms_api_rest/security/ir.model.access.csv deleted file mode 100644 index e97ddc130e..0000000000 --- a/pms_api_rest/security/ir.model.access.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_jwt_access_token,Read jwt access token,model_jwt_provider_access_token,,1,0,0,0 diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 87c212ea52..064c89aae5 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -5,4 +5,8 @@ from . import partner_services from . import reservation_services +<<<<<<< HEAD from . import property_services +======= +from . import login_service +>>>>>>> a4394db3... [REF] pms-api-rest: add controller 4 login diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index 6132f87562..d7d3d34ae1 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -7,7 +7,7 @@ class PmsCalendarService(Component): _inherit = "base.rest.service" - _name = "pms.calendar.service" + _name = "pms.private.services" _usage = "calendar" _collection = "pms.reservation.service" diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 1918bf0784..242542d123 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -9,7 +9,7 @@ class PmsFolioService(Component): _inherit = "base.rest.service" _name = "pms.folio.service" _usage = "folios" - _collection = "pms.reservation.service" + _collection = "pms.private.services" @restapi.method( [ diff --git a/pms_api_rest/services/login_service.py b/pms_api_rest/services/login_service.py new file mode 100644 index 0000000000..82a431a1f2 --- /dev/null +++ b/pms_api_rest/services/login_service.py @@ -0,0 +1,116 @@ +import time + +from jose import jwt + +from odoo import _ +from odoo.exceptions import ValidationError, AccessDenied, UserError + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsPartnerService(Component): + _inherit = "base.rest.service" + _name = "pms.auth.service" + _usage = "login" + _collection = "pms.public.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), + ) + def aa(self, user): + + user_record = ( + self.env["res.users"].sudo().search([("login", "=", user.username)]) + ) + + if not user_record: + ValidationError(_("user or password not valid")) + try: + user_record.with_user(user_record)._check_credentials(user.password, None) + + except Exception as e: + raise UserError("") + + PmsApiRestUserOutput = self.env.datamodels["pms.api.rest.user.output"] + expiration_date = time.time() + 36660 + token = jwt.encode( + { + "aud": "api_pms", + "iss": "pms", + "exp": expiration_date, + "username": user.username, + "password": user.password, + }, + key="pms_secret_key_example", + algorithm=jwt.ALGORITHMS.HS256, + ) + # return PmsApiRestUserOutput(token=token) + return token + + def user_error(self): + """ + Simulate an odoo.exceptions.UserError + Should be translated into BadRequest with a description into the json + body + """ + raise UserError(_("UserError message")) + + # Validator + def _validator_user_error(self): + return {} + + def _validator_return_user_error(self): + return {} + + def _validator_validation_error(self): + return {} + + def _validator_return_validation_error(self): + return {} + + def _validator_session_expired(self): + return {} + + def _validator_return_session_expired(self): + return {} + + def _validator_missing_error(self): + return {} + + def _validator_return_missing_error(self): + return {} + + def _validator_access_error(self): + return {} + + def _validator_return_access_error(self): + return {} + + def _validator_access_denied(self): + return {} + + def _validator_return_access_denied(self): + return {} + + def _validator_http_exception(self): + return {} + + def _validator_return_http_exception(self): + return {} + + def _validator_bare_exception(self): + return {} + + def _validator_return_bare_exception(self): + return {} diff --git a/pms_api_rest/services/partner_services.py b/pms_api_rest/services/partner_services.py index 1b7a45cca0..1940da10be 100644 --- a/pms_api_rest/services/partner_services.py +++ b/pms_api_rest/services/partner_services.py @@ -7,7 +7,7 @@ class PmsPartnerService(Component): _inherit = "base.rest.service" _name = "pms.partner.service" _usage = "partners" - _collection = "pms.reservation.service" + _collection = "pms.private.services" @restapi.method( [ diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index f400ee8f92..e922d85b2b 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -9,7 +9,7 @@ class PmsRoomService(Component): _inherit = "base.rest.service" _name = "pms.reservation.service" _usage = "reservations" - _collection = "pms.reservation.service" + _collection = "pms.private.services" @restapi.method( [ diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py index 1727528a18..4e10a51e93 100644 --- a/pms_api_rest/services/room_services.py +++ b/pms_api_rest/services/room_services.py @@ -7,7 +7,7 @@ class PmsRoomService(Component): _inherit = "base.rest.service" _name = "pms.room.service" _usage = "rooms" - _collection = "pms.reservation.service" + _collection = "pms.private.services" @restapi.method( [ diff --git a/pms_api_rest/services/room_type_services.py b/pms_api_rest/services/room_type_services.py index b628da6256..4ca879a549 100644 --- a/pms_api_rest/services/room_type_services.py +++ b/pms_api_rest/services/room_type_services.py @@ -7,7 +7,7 @@ class PmsRoomTypeService(Component): _inherit = "base.rest.service" _name = "pms.room.type.service" _usage = "room-types" - _collection = "pms.reservation.service" + _collection = "pms.private.services" @restapi.method( [ From fbdace537ab945a94a96dc2bfd09ad59d14ed517 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 27 Dec 2021 16:31:09 +0100 Subject: [PATCH 030/547] [FIX] fix login controller, fix public & private endpoints, add cors --- pms_api_rest/controllers/pms_rest.py | 2 + pms_api_rest/services/calendar_service.py | 3 +- pms_api_rest/services/folio_services.py | 4 -- pms_api_rest/services/login_service.py | 71 ++----------------- pms_api_rest/services/property_services.py | 11 +-- pms_api_rest/services/reservation_services.py | 2 - pms_api_rest/services/room_services.py | 1 - pms_api_rest/services/room_type_services.py | 1 - 8 files changed, 10 insertions(+), 85 deletions(-) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index 5a738e502a..d3389b247f 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -5,9 +5,11 @@ class BaseRestPrivateApiController(main.RestController): _root_path = "/api/" _collection_name = "pms.private.services" _default_auth = "jwt_api_pms" + _default_cors = "*" class BaseRestPublicApiController(main.RestController): _root_path = "/auth/" _collection_name = "pms.public.services" _default_auth = "public" + _default_cors = "*" diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index d7d3d34ae1..a420d767c6 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -9,7 +9,7 @@ class PmsCalendarService(Component): _inherit = "base.rest.service" _name = "pms.private.services" _usage = "calendar" - _collection = "pms.reservation.service" + _collection = "pms.private.services" @restapi.method( [ @@ -62,7 +62,6 @@ def get_calendar(self, calendar_search_param): ) ], input_param=Datamodel("pms.calendar.swap.info", is_list=False), - auth="public", ) def swap_reservation_slices(self, swap_info): room_id_a = swap_info.roomIdA diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 242542d123..0c80a345b6 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -22,7 +22,6 @@ class PmsFolioService(Component): ], input_param=Datamodel("pms.folio.search.param"), output_param=Datamodel("pms.folio.info", is_list=True), - auth="public", ) def get_folios(self, folio_search_param): domain = [] @@ -117,7 +116,6 @@ def get_folios(self, folio_search_param): ) ], output_param=Datamodel("pms.reservation.info"), - auth="public", ) def get_reservation(self, folio_id, reservation_id): reservation = ( @@ -190,7 +188,6 @@ def get_reservation(self, folio_id, reservation_id): ) ], output_param=Datamodel("pms.checkin.partner.info", is_list=True), - auth="public", ) def get_checkin_partners(self, folio_id, reservation_id): reservation = ( @@ -236,7 +233,6 @@ def get_checkin_partners(self, folio_id, reservation_id): ) ], output_param=Datamodel("pms.payment.info", is_list=True), - auth="public", ) def get_folio_payments(self, folio_id): folio = ( diff --git a/pms_api_rest/services/login_service.py b/pms_api_rest/services/login_service.py index 82a431a1f2..3176b17ba2 100644 --- a/pms_api_rest/services/login_service.py +++ b/pms_api_rest/services/login_service.py @@ -26,9 +26,9 @@ class PmsPartnerService(Component): ) ], input_param=Datamodel("pms.api.rest.user.input", is_list=False), - # output_param=Datamodel("pms.api.rest.user.output", is_list=False), + output_param=Datamodel("pms.api.rest.user.output", is_list=False), ) - def aa(self, user): + def login(self, user): user_record = ( self.env["res.users"].sudo().search([("login", "=", user.username)]) @@ -36,12 +36,7 @@ def aa(self, user): if not user_record: ValidationError(_("user or password not valid")) - try: - user_record.with_user(user_record)._check_credentials(user.password, None) - - except Exception as e: - raise UserError("") - + user_record.with_user(user_record)._check_credentials(user.password, None) PmsApiRestUserOutput = self.env.datamodels["pms.api.rest.user.output"] expiration_date = time.time() + 36660 token = jwt.encode( @@ -55,62 +50,4 @@ def aa(self, user): key="pms_secret_key_example", algorithm=jwt.ALGORITHMS.HS256, ) - # return PmsApiRestUserOutput(token=token) - return token - - def user_error(self): - """ - Simulate an odoo.exceptions.UserError - Should be translated into BadRequest with a description into the json - body - """ - raise UserError(_("UserError message")) - - # Validator - def _validator_user_error(self): - return {} - - def _validator_return_user_error(self): - return {} - - def _validator_validation_error(self): - return {} - - def _validator_return_validation_error(self): - return {} - - def _validator_session_expired(self): - return {} - - def _validator_return_session_expired(self): - return {} - - def _validator_missing_error(self): - return {} - - def _validator_return_missing_error(self): - return {} - - def _validator_access_error(self): - return {} - - def _validator_return_access_error(self): - return {} - - def _validator_access_denied(self): - return {} - - def _validator_return_access_denied(self): - return {} - - def _validator_http_exception(self): - return {} - - def _validator_return_http_exception(self): - return {} - - def _validator_bare_exception(self): - return {} - - def _validator_return_bare_exception(self): - return {} + return PmsApiRestUserOutput(token=token) diff --git a/pms_api_rest/services/property_services.py b/pms_api_rest/services/property_services.py index 926cdd7949..afdc374700 100644 --- a/pms_api_rest/services/property_services.py +++ b/pms_api_rest/services/property_services.py @@ -1,5 +1,3 @@ -from datetime import datetime - from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component @@ -9,7 +7,7 @@ class PmsPropertyComponent(Component): _inherit = "base.rest.service" _name = "pms.property.service" _usage = "properties" - _collection = "pms.reservation.service" + _collection = "pms.private.services" @restapi.method( [ @@ -22,9 +20,8 @@ class PmsPropertyComponent(Component): ], input_param=Datamodel("pms.property.search.param"), output_param=Datamodel("pms.property.info", is_list=True), - auth="public", ) - def get_properties(self,property_search_param): + def get_properties(self, property_search_param): domain = [] if property_search_param.name: domain.append(("name", "like", property_search_param.name)) @@ -58,7 +55,6 @@ def get_properties(self,property_search_param): ) ], output_param=Datamodel("pms.property.info"), - auth="public", ) def get_property(self, property_id): pms_property = ( @@ -86,8 +82,7 @@ def get_property(self, property_id): "GET", ) ], - output_param=Datamodel("pms.account.journal.info",is_list=True), - auth="public", + output_param=Datamodel("pms.account.journal.info", is_list=True), ) def get_method_payments_property(self, property_id): diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index e922d85b2b..db1b3c1121 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -21,7 +21,6 @@ class PmsRoomService(Component): ) ], output_param=Datamodel("pms.reservation.info", is_list=True), - auth="public", ) def get_reservations(self): domain = [] @@ -55,7 +54,6 @@ def get_reservations(self): ) ], input_param=Datamodel("pms.calendar.changes", is_list=False), - auth="public", ) def move_reservation_line(self, reservation_id, reservation_lines_changes): diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py index 4e10a51e93..ca981708e3 100644 --- a/pms_api_rest/services/room_services.py +++ b/pms_api_rest/services/room_services.py @@ -20,7 +20,6 @@ class PmsRoomService(Component): ], input_param=Datamodel("pms.room.search.param"), output_param=Datamodel("pms.room.info", is_list=True), - auth="public", ) def get_rooms(self, room_search_param): domain = [] diff --git a/pms_api_rest/services/room_type_services.py b/pms_api_rest/services/room_type_services.py index 4ca879a549..e6ef7dc232 100644 --- a/pms_api_rest/services/room_type_services.py +++ b/pms_api_rest/services/room_type_services.py @@ -20,7 +20,6 @@ class PmsRoomTypeService(Component): ], input_param=Datamodel("pms.room.search.param"), output_param=Datamodel("pms.room.info", is_list=True), - auth="public", ) def get_room_types(self, room_type_search_param): domain = [] From c2d432d941f8e9f6040c5abfbf2990763b0041cc Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 27 Dec 2021 16:32:59 +0100 Subject: [PATCH 031/547] [FIX] pms-api-rest: fix precommit & PEP --- pms_api_rest/services/folio_services.py | 14 ++++++++------ pms_api_rest/services/login_service.py | 2 +- pms_api_rest/services/property_services.py | 10 +++------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 0c80a345b6..a985335cf7 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -98,9 +98,13 @@ def get_folios(self, folio_search_param): pendingAmount=folio.pending_amount, reservations=[] if not reservations else reservations, salesPerson=folio.user_id.name if folio.user_id else "", - paymentState=dict(folio.fields_get(["payment_state"])["payment_state"]["selection"])[ - folio.payment_state - ] if folio.payment_state else "", + paymentState=dict( + folio.fields_get(["payment_state"])["payment_state"][ + "selection" + ] + )[folio.payment_state] + if folio.payment_state + else "", propertyId=folio.pms_property_id, ) ) @@ -235,9 +239,7 @@ def get_checkin_partners(self, folio_id, reservation_id): output_param=Datamodel("pms.payment.info", is_list=True), ) def get_folio_payments(self, folio_id): - folio = ( - self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) - ) + folio = self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) payments = [] PmsPaymentInfo = self.env.datamodels["pms.payment.info"] if not folio: diff --git a/pms_api_rest/services/login_service.py b/pms_api_rest/services/login_service.py index 3176b17ba2..67fad6570d 100644 --- a/pms_api_rest/services/login_service.py +++ b/pms_api_rest/services/login_service.py @@ -3,7 +3,7 @@ from jose import jwt from odoo import _ -from odoo.exceptions import ValidationError, AccessDenied, UserError +from odoo.exceptions import ValidationError from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel diff --git a/pms_api_rest/services/property_services.py b/pms_api_rest/services/property_services.py index afdc374700..3ad41d29fa 100644 --- a/pms_api_rest/services/property_services.py +++ b/pms_api_rest/services/property_services.py @@ -86,17 +86,13 @@ def get_property(self, property_id): ) def get_method_payments_property(self, property_id): - property = ( - self.env["pms.property"].sudo().search([("id", "=", property_id)]) - ) + pms_property = self.env["pms.property"].sudo().search([("id", "=", property_id)]) PmsAccountJournalInfo = self.env.datamodels["pms.account.journal.info"] res = [] - if not property: + if not pms_property: pass else: - for method in property._get_payment_methods( - automatic_included=True - ): + for method in pms_property._get_payment_methods(automatic_included=True): payment_method = ( self.env["account.journal"].sudo().search([("id", "=", method.id)]) ) From d1686c087e8863779046a5161c90e745533456bf Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 27 Dec 2021 20:55:13 +0100 Subject: [PATCH 032/547] [REF] pms-api-rest: refactor services --- pms_api_rest/controllers/pms_rest.py | 9 +-------- pms_api_rest/services/calendar_service.py | 6 ++++-- pms_api_rest/services/folio_services.py | 2 +- pms_api_rest/services/login_service.py | 3 ++- pms_api_rest/services/partner_services.py | 3 ++- pms_api_rest/services/property_services.py | 8 +++++++- pms_api_rest/services/reservation_services.py | 6 +++++- pms_api_rest/services/room_services.py | 4 +++- pms_api_rest/services/room_type_services.py | 4 +++- 9 files changed, 28 insertions(+), 17 deletions(-) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index d3389b247f..3bfda003f8 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -3,13 +3,6 @@ class BaseRestPrivateApiController(main.RestController): _root_path = "/api/" - _collection_name = "pms.private.services" - _default_auth = "jwt_api_pms" - _default_cors = "*" - - -class BaseRestPublicApiController(main.RestController): - _root_path = "/auth/" - _collection_name = "pms.public.services" + _collection_name = "pms.services" _default_auth = "public" _default_cors = "*" diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index a420d767c6..ce0c02cf3a 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -9,7 +9,7 @@ class PmsCalendarService(Component): _inherit = "base.rest.service" _name = "pms.private.services" _usage = "calendar" - _collection = "pms.private.services" + _collection = "pms.services" @restapi.method( [ @@ -22,7 +22,8 @@ class PmsCalendarService(Component): ], input_param=Datamodel("pms.calendar.search.param"), output_param=Datamodel("pms.calendar.info", is_list=True), - auth="public", + auth="jwt_api_pms", + ) def get_calendar(self, calendar_search_param): domain = list() @@ -62,6 +63,7 @@ def get_calendar(self, calendar_search_param): ) ], input_param=Datamodel("pms.calendar.swap.info", is_list=False), + auth="jwt_api_pms", ) def swap_reservation_slices(self, swap_info): room_id_a = swap_info.roomIdA diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index a985335cf7..22d360eddc 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -9,7 +9,7 @@ class PmsFolioService(Component): _inherit = "base.rest.service" _name = "pms.folio.service" _usage = "folios" - _collection = "pms.private.services" + _collection = "pms.services" @restapi.method( [ diff --git a/pms_api_rest/services/login_service.py b/pms_api_rest/services/login_service.py index 67fad6570d..bc1a98d0f3 100644 --- a/pms_api_rest/services/login_service.py +++ b/pms_api_rest/services/login_service.py @@ -14,7 +14,7 @@ class PmsPartnerService(Component): _inherit = "base.rest.service" _name = "pms.auth.service" _usage = "login" - _collection = "pms.public.services" + _collection = "pms.services" @restapi.method( [ @@ -27,6 +27,7 @@ class PmsPartnerService(Component): ], input_param=Datamodel("pms.api.rest.user.input", is_list=False), output_param=Datamodel("pms.api.rest.user.output", is_list=False), + auth="public", ) def login(self, user): diff --git a/pms_api_rest/services/partner_services.py b/pms_api_rest/services/partner_services.py index 1940da10be..ec48cd19ee 100644 --- a/pms_api_rest/services/partner_services.py +++ b/pms_api_rest/services/partner_services.py @@ -7,7 +7,7 @@ class PmsPartnerService(Component): _inherit = "base.rest.service" _name = "pms.partner.service" _usage = "partners" - _collection = "pms.private.services" + _collection = "pms.services" @restapi.method( [ @@ -19,6 +19,7 @@ class PmsPartnerService(Component): ) ], output_param=Datamodel("pms.partner.info", is_list=True), + auth="jwt_api_pms", ) def get_partners(self): domain = [] diff --git a/pms_api_rest/services/property_services.py b/pms_api_rest/services/property_services.py index 3ad41d29fa..60b270940f 100644 --- a/pms_api_rest/services/property_services.py +++ b/pms_api_rest/services/property_services.py @@ -7,7 +7,7 @@ class PmsPropertyComponent(Component): _inherit = "base.rest.service" _name = "pms.property.service" _usage = "properties" - _collection = "pms.private.services" + _collection = "pms.services" @restapi.method( [ @@ -20,6 +20,8 @@ class PmsPropertyComponent(Component): ], input_param=Datamodel("pms.property.search.param"), output_param=Datamodel("pms.property.info", is_list=True), + auth="jwt_api_pms", + ) def get_properties(self, property_search_param): domain = [] @@ -55,6 +57,8 @@ def get_properties(self, property_search_param): ) ], output_param=Datamodel("pms.property.info"), + auth="jwt_api_pms", + ) def get_property(self, property_id): pms_property = ( @@ -83,6 +87,8 @@ def get_property(self, property_id): ) ], output_param=Datamodel("pms.account.journal.info", is_list=True), + auth="jwt_api_pms", + ) def get_method_payments_property(self, property_id): diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index db1b3c1121..c97821c22d 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -9,7 +9,7 @@ class PmsRoomService(Component): _inherit = "base.rest.service" _name = "pms.reservation.service" _usage = "reservations" - _collection = "pms.private.services" + _collection = "pms.services" @restapi.method( [ @@ -21,6 +21,8 @@ class PmsRoomService(Component): ) ], output_param=Datamodel("pms.reservation.info", is_list=True), + auth="jwt_api_pms", + ) def get_reservations(self): domain = [] @@ -54,6 +56,8 @@ def get_reservations(self): ) ], input_param=Datamodel("pms.calendar.changes", is_list=False), + auth="jwt_api_pms", + ) def move_reservation_line(self, reservation_id, reservation_lines_changes): diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py index ca981708e3..62ca6fd4b5 100644 --- a/pms_api_rest/services/room_services.py +++ b/pms_api_rest/services/room_services.py @@ -7,7 +7,7 @@ class PmsRoomService(Component): _inherit = "base.rest.service" _name = "pms.room.service" _usage = "rooms" - _collection = "pms.private.services" + _collection = "pms.services" @restapi.method( [ @@ -20,6 +20,8 @@ class PmsRoomService(Component): ], 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 = [] diff --git a/pms_api_rest/services/room_type_services.py b/pms_api_rest/services/room_type_services.py index e6ef7dc232..6682504404 100644 --- a/pms_api_rest/services/room_type_services.py +++ b/pms_api_rest/services/room_type_services.py @@ -7,7 +7,7 @@ class PmsRoomTypeService(Component): _inherit = "base.rest.service" _name = "pms.room.type.service" _usage = "room-types" - _collection = "pms.private.services" + _collection = "pms.services" @restapi.method( [ @@ -20,6 +20,8 @@ class PmsRoomTypeService(Component): ], input_param=Datamodel("pms.room.search.param"), output_param=Datamodel("pms.room.info", is_list=True), + auth="jwt_api_pms", + ) def get_room_types(self, room_type_search_param): domain = [] From 3adfe0aee9570f050e13a71534e7ef1932dd8ea6 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 28 Dec 2021 15:16:05 +0100 Subject: [PATCH 033/547] [IMP] pms-api-rest: add several fields reserv. line service --- pms_api_rest/__manifest__.py | 2 +- pms_api_rest/datamodels/pms_calendar_info.py | 3 +++ pms_api_rest/services/calendar_service.py | 7 +++++-- pms_api_rest/services/property_services.py | 7 +++---- pms_api_rest/services/reservation_services.py | 18 +++++++++++++++--- pms_api_rest/services/room_services.py | 1 - pms_api_rest/services/room_type_services.py | 1 - requirements.txt | 1 + 8 files changed, 28 insertions(+), 12 deletions(-) diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 2529e90b0e..7bf98aa078 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -14,7 +14,7 @@ "auth_jwt_login", ], "external_dependencies": { - "python": ["jwt", "simplejson", "marshmallow"], + "python": ["jwt", "simplejson", "marshmallow", "jose"], }, "data": ["data/auth_jwt_validator.xml"], "installable": True, diff --git a/pms_api_rest/datamodels/pms_calendar_info.py b/pms_api_rest/datamodels/pms_calendar_info.py index 37d95b629d..089fbea094 100644 --- a/pms_api_rest/datamodels/pms_calendar_info.py +++ b/pms_api_rest/datamodels/pms_calendar_info.py @@ -10,3 +10,6 @@ class PmsCalendarInfo(Datamodel): roomId = fields.Integer(required=False, allow_none=True) partnerId = fields.Integer(required=False, allow_none=True) reservationId = fields.Integer(required=False, allow_none=True) + isFirstDay = fields.Boolean(required=False, allow_none=True) + isLastDay = fields.Boolean(required=False, allow_none=True) + totalPrice = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index ce0c02cf3a..3ba49c1963 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -23,7 +23,6 @@ class PmsCalendarService(Component): input_param=Datamodel("pms.calendar.search.param"), output_param=Datamodel("pms.calendar.info", is_list=True), auth="jwt_api_pms", - ) def get_calendar(self, calendar_search_param): domain = list() @@ -49,6 +48,10 @@ def get_calendar(self, calendar_search_param): date=datetime.combine(line.date, datetime.min.time()).isoformat(), partnerId=line.reservation_id.partner_id.id, reservationId=line.reservation_id, + isFirstDay=line.reservation_id.checkin == line.date, + isLastDay=line.reservation_id.checkout + == (line.date + timedelta(days=1)), + totalPrice=line.reservation_id.price_total, ) ) return result_lines diff --git a/pms_api_rest/services/property_services.py b/pms_api_rest/services/property_services.py index 60b270940f..e5573a61f4 100644 --- a/pms_api_rest/services/property_services.py +++ b/pms_api_rest/services/property_services.py @@ -21,7 +21,6 @@ class PmsPropertyComponent(Component): input_param=Datamodel("pms.property.search.param"), output_param=Datamodel("pms.property.info", is_list=True), auth="jwt_api_pms", - ) def get_properties(self, property_search_param): domain = [] @@ -58,7 +57,6 @@ def get_properties(self, property_search_param): ], output_param=Datamodel("pms.property.info"), auth="jwt_api_pms", - ) def get_property(self, property_id): pms_property = ( @@ -88,11 +86,12 @@ def get_property(self, property_id): ], output_param=Datamodel("pms.account.journal.info", is_list=True), auth="jwt_api_pms", - ) def get_method_payments_property(self, property_id): - pms_property = self.env["pms.property"].sudo().search([("id", "=", property_id)]) + pms_property = ( + self.env["pms.property"].sudo().search([("id", "=", property_id)]) + ) PmsAccountJournalInfo = self.env.datamodels["pms.account.journal.info"] res = [] if not pms_property: diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index c97821c22d..96fc4da35a 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -22,7 +22,6 @@ class PmsRoomService(Component): ], output_param=Datamodel("pms.reservation.info", is_list=True), auth="jwt_api_pms", - ) def get_reservations(self): domain = [] @@ -57,7 +56,6 @@ def get_reservations(self): ], input_param=Datamodel("pms.calendar.changes", is_list=False), auth="jwt_api_pms", - ) def move_reservation_line(self, reservation_id, reservation_lines_changes): @@ -96,3 +94,17 @@ def move_reservation_line(self, reservation_id, reservation_lines_changes): 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 diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py index 62ca6fd4b5..70ea0f2bf7 100644 --- a/pms_api_rest/services/room_services.py +++ b/pms_api_rest/services/room_services.py @@ -21,7 +21,6 @@ class PmsRoomService(Component): 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 = [] diff --git a/pms_api_rest/services/room_type_services.py b/pms_api_rest/services/room_type_services.py index 6682504404..45bd33c409 100644 --- a/pms_api_rest/services/room_type_services.py +++ b/pms_api_rest/services/room_type_services.py @@ -21,7 +21,6 @@ class PmsRoomTypeService(Component): input_param=Datamodel("pms.room.search.param"), output_param=Datamodel("pms.room.info", is_list=True), auth="jwt_api_pms", - ) def get_room_types(self, room_type_search_param): domain = [] diff --git a/requirements.txt b/requirements.txt index ba49ce1968..093f58f35f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ # generated from manifests external_dependencies bs4 +jose jwt marshmallow pycountry From 6a7c84fd35cc5f73cb995a63120faaa5c812d487 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 28 Dec 2021 19:38:34 +0100 Subject: [PATCH 034/547] [IMP] pms-api-rest: add notif. rt capacity --- pms_api_rest/datamodels/pms_calendar_info.py | 1 + pms_api_rest/datamodels/pms_room_info.py | 1 + pms_api_rest/services/calendar_service.py | 3 ++- pms_api_rest/services/room_services.py | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_calendar_info.py b/pms_api_rest/datamodels/pms_calendar_info.py index 089fbea094..029b7e0dc3 100644 --- a/pms_api_rest/datamodels/pms_calendar_info.py +++ b/pms_api_rest/datamodels/pms_calendar_info.py @@ -13,3 +13,4 @@ class PmsCalendarInfo(Datamodel): isFirstDay = fields.Boolean(required=False, allow_none=True) isLastDay = fields.Boolean(required=False, allow_none=True) totalPrice = fields.Float(required=False, allow_none=True) + numNotifications = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room_info.py b/pms_api_rest/datamodels/pms_room_info.py index 5a5d5a4650..d6a2603fb3 100644 --- a/pms_api_rest/datamodels/pms_room_info.py +++ b/pms_api_rest/datamodels/pms_room_info.py @@ -8,3 +8,4 @@ class PmsRoomInfo(Datamodel): 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) diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index 3ba49c1963..93619d7059 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -46,12 +46,13 @@ def get_calendar(self, calendar_search_param): id=line.id, roomId=line.room_id.id, date=datetime.combine(line.date, datetime.min.time()).isoformat(), - partnerId=line.reservation_id.partner_id.id, + partnerId=line.reservation_id.partner_id.id or None, reservationId=line.reservation_id, isFirstDay=line.reservation_id.checkin == line.date, isLastDay=line.reservation_id.checkout == (line.date + timedelta(days=1)), totalPrice=line.reservation_id.price_total, + numNotifications=len(line.reservation_id.message_ids), ) ) return result_lines diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py index 70ea0f2bf7..8bc61e2152 100644 --- a/pms_api_rest/services/room_services.py +++ b/pms_api_rest/services/room_services.py @@ -43,6 +43,7 @@ def get_rooms(self, room_search_param): id=room.id, name=room.name, roomTypeId=room.room_type_id, + capacity=room.capacity, ) ) return result_rooms From 4d977d7b869e6055ea19dd0477d963ba9f4f084b Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 28 Dec 2021 23:12:37 +0100 Subject: [PATCH 035/547] [IMP] pms-api-rest: add calendar fields --- pms_api_rest/datamodels/pms_calendar_info.py | 7 +++++++ pms_api_rest/services/calendar_service.py | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_calendar_info.py b/pms_api_rest/datamodels/pms_calendar_info.py index 029b7e0dc3..38fc751217 100644 --- a/pms_api_rest/datamodels/pms_calendar_info.py +++ b/pms_api_rest/datamodels/pms_calendar_info.py @@ -6,11 +6,18 @@ 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) + toAssign = fields.Boolean(required=False, allow_none=True) partnerId = fields.Integer(required=False, allow_none=True) + partnerName = fields.String(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) isFirstDay = fields.Boolean(required=False, allow_none=True) isLastDay = 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) diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index 93619d7059..bdcdc7c55e 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -44,15 +44,22 @@ def get_calendar(self, calendar_search_param): result_lines.append( PmsCalendarInfo( id=line.id, - roomId=line.room_id.id, + state=line.reservation_id.state, date=datetime.combine(line.date, datetime.min.time()).isoformat(), + roomId=line.room_id.id, + toAssign=line.reservation_id.to_assign, partnerId=line.reservation_id.partner_id.id or None, + partnerName=line.reservation_id.partner_name or None, reservationId=line.reservation_id, + reservationName=line.reservation_id.name, + reservationType=line.reservation_id.reservation_type, isFirstDay=line.reservation_id.checkin == line.date, isLastDay=line.reservation_id.checkout == (line.date + timedelta(days=1)), totalPrice=line.reservation_id.price_total, + pendingPayment=line.reservation_id.folio_pending_amount, numNotifications=len(line.reservation_id.message_ids), + adults=line.reservation_id.adults, ) ) return result_lines From 8eee99f5d1e5fd3722243febbdd8ffa6a2e6b7d6 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 28 Dec 2021 23:30:19 +0100 Subject: [PATCH 036/547] [IMP] pms-api-rest: filter folios by date --- pms_api_rest/datamodels/pms_folio_search_param.py | 4 ++-- pms_api_rest/services/folio_services.py | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio_search_param.py b/pms_api_rest/datamodels/pms_folio_search_param.py index 027e65660b..5a37c60034 100644 --- a/pms_api_rest/datamodels/pms_folio_search_param.py +++ b/pms_api_rest/datamodels/pms_folio_search_param.py @@ -6,5 +6,5 @@ class PmsFolioSearchParam(Datamodel): _name = "pms.folio.search.param" - id = fields.Integer(required=False, allow_none=False) - name = fields.String(required=False, allow_none=False) + date_from = fields.String(required=False, allow_none=True) + date_to = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 22d360eddc..f202d1f813 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -24,18 +24,21 @@ class PmsFolioService(Component): output_param=Datamodel("pms.folio.info", is_list=True), ) def get_folios(self, folio_search_param): - domain = [] - if folio_search_param.name: - domain.append(("name", "like", folio_search_param.name)) - if folio_search_param.id: - domain.append(("id", "=", folio_search_param.id)) + domain = list() + domain.append(("checkin", ">=", folio_search_param.date_from)) + domain.append(("checkout", "<", folio_search_param.date_to)) result_folios = [] + + reservations_result = ( + self.env["pms.reservation"].sudo().search(domain).mapped("folio_id").ids + ) + PmsFolioInfo = self.env.datamodels["pms.folio.info"] for folio in ( self.env["pms.folio"] .sudo() .search( - domain, + [("id", "in", reservations_result)], ) ): reservations = [] From a6a5f42b6cc24c8028f43ebc4d2c86bf3060583b Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 29 Dec 2021 10:02:01 +0100 Subject: [PATCH 037/547] [IMP] pms-api-rest: tsts cors --- pms_api_rest/controllers/pms_rest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index 3bfda003f8..cb3481c411 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -5,4 +5,5 @@ class BaseRestPrivateApiController(main.RestController): _root_path = "/api/" _collection_name = "pms.services" _default_auth = "public" + _default_cors = "*" From f696c4ac28286a116cb094c6690dff58db4e5c17 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 30 Dec 2021 09:27:32 +0100 Subject: [PATCH 038/547] [IMP] pms-api-rest: add roomtypeName & splitted @ calendar lines --- pms_api_rest/datamodels/pms_calendar_info.py | 2 ++ pms_api_rest/services/calendar_service.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_calendar_info.py b/pms_api_rest/datamodels/pms_calendar_info.py index 38fc751217..f5ccf56cf1 100644 --- a/pms_api_rest/datamodels/pms_calendar_info.py +++ b/pms_api_rest/datamodels/pms_calendar_info.py @@ -9,7 +9,9 @@ class PmsCalendarInfo(Datamodel): 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) reservationId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/calendar_service.py index bdcdc7c55e..47be040fa0 100644 --- a/pms_api_rest/services/calendar_service.py +++ b/pms_api_rest/services/calendar_service.py @@ -47,7 +47,9 @@ def get_calendar(self, calendar_search_param): state=line.reservation_id.state, date=datetime.combine(line.date, datetime.min.time()).isoformat(), roomId=line.room_id.id, + roomTypeName=str(line.reservation_id.room_type_id.default_code), toAssign=line.reservation_id.to_assign, + splitted=line.reservation_id.splitted, partnerId=line.reservation_id.partner_id.id or None, partnerName=line.reservation_id.partner_name or None, reservationId=line.reservation_id, From 0a100efc7cb3084f41c21e2eee8931d3e9dfe2a5 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 31 Dec 2021 12:34:14 +0100 Subject: [PATCH 039/547] [FIX] pms-api-rest: fix login service when user is not correct --- pms_api_rest/services/login_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/login_service.py b/pms_api_rest/services/login_service.py index bc1a98d0f3..7bd95fd84f 100644 --- a/pms_api_rest/services/login_service.py +++ b/pms_api_rest/services/login_service.py @@ -36,7 +36,7 @@ def login(self, user): ) if not user_record: - ValidationError(_("user or password not valid")) + raise ValidationError(_("user or password not valid")) user_record.with_user(user_record)._check_credentials(user.password, None) PmsApiRestUserOutput = self.env.datamodels["pms.api.rest.user.output"] expiration_date = time.time() + 36660 From 7bfa64da1e756fc43d9011be8373b17b6192decc Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 3 Jan 2022 10:25:28 +0100 Subject: [PATCH 040/547] [FIX] pms-api-rest: fix login service cors --- pms_api_rest/services/login_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/services/login_service.py b/pms_api_rest/services/login_service.py index 7bd95fd84f..ac45cc993d 100644 --- a/pms_api_rest/services/login_service.py +++ b/pms_api_rest/services/login_service.py @@ -28,6 +28,7 @@ class PmsPartnerService(Component): 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): From 627134e1b5dab6573c0f853dfedd347c5ca052e9 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 7 Jan 2022 10:17:06 +0100 Subject: [PATCH 041/547] [IMP] pms_api_rest: add post reservations --- .../datamodels/pms_reservation_info.py | 15 ++++---- pms_api_rest/services/folio_services.py | 35 +++++++++++++++++-- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/pms_api_rest/datamodels/pms_reservation_info.py b/pms_api_rest/datamodels/pms_reservation_info.py index c36e7f872d..ade4d72262 100644 --- a/pms_api_rest/datamodels/pms_reservation_info.py +++ b/pms_api_rest/datamodels/pms_reservation_info.py @@ -6,21 +6,20 @@ class PmsReservationInfo(Datamodel): _name = "pms.reservation.info" id = fields.Integer(required=False, allow_none=True) -<<<<<<< HEAD - price = fields.Float(required=False, allow_none=True) - checkin = fields.String(required=False, allow_none=True) - checkout = fields.String(required=False, allow_none=True) -======= partner = fields.String(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) checkin = fields.String(required=False, allow_none=True) checkout = fields.String(required=False, allow_none=True) - roomTypeId = fields.String(required=False, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + roomTypeName = fields.String(required=False, allow_none=True) preferredRoomId = fields.String(required=False, allow_none=True) priceTotal = fields.Float(required=False, allow_none=True) priceOnlyServices = fields.Float(required=False, allow_none=True) priceOnlyRoom = fields.Float(required=False, allow_none=True) - pricelist = fields.String(required=False, allow_none=True) + pricelistName = fields.String(required=False, allow_none=True) + pricelistId = fields.Integer(required=False, allow_none=True) services = fields.List(fields.Dict(required=False, allow_none=True)) messages = fields.List(fields.Dict(required=False, allow_none=True)) ->>>>>>> d6e6a667... [IMP] pms_api_rest: add get_reservation and get_checkin_partners + property = fields.Integer(required=False, allow_none=True) + boardServiceId = fields.Integer(required=False, allow_none=True) + channelTypeId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index f202d1f813..251cc06f65 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -22,6 +22,7 @@ class PmsFolioService(Component): ], input_param=Datamodel("pms.folio.search.param"), output_param=Datamodel("pms.folio.info", is_list=True), + auth="jwt_api_pms", ) def get_folios(self, folio_search_param): domain = list() @@ -123,6 +124,7 @@ def get_folios(self, folio_search_param): ) ], output_param=Datamodel("pms.reservation.info"), + auth="jwt_api_pms", ) def get_reservation(self, folio_id, reservation_id): reservation = ( @@ -168,7 +170,7 @@ def get_reservation(self, folio_id, reservation_id): preferredRoomId=reservation.preferred_room_id.name if reservation.preferred_room_id else "", - roomTypeId=reservation.room_type_id.name + roomTypeName=reservation.room_type_id.name if reservation.room_type_id else "", name=reservation.name, @@ -177,7 +179,7 @@ def get_reservation(self, folio_id, reservation_id): if reservation.price_services else 0.0, priceOnlyRoom=reservation.price_total, - pricelist=reservation.pricelist_id.name + pricelistName=reservation.pricelist_id.name if reservation.pricelist_id else "", services=services if services else [], @@ -195,6 +197,7 @@ def get_reservation(self, folio_id, reservation_id): ) ], output_param=Datamodel("pms.checkin.partner.info", is_list=True), + auth="jwt_api_pms", ) def get_checkin_partners(self, folio_id, reservation_id): reservation = ( @@ -240,6 +243,7 @@ def get_checkin_partners(self, folio_id, reservation_id): ) ], output_param=Datamodel("pms.payment.info", is_list=True), + auth="jwt_api_pms", ) def get_folio_payments(self, folio_id): folio = self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) @@ -276,3 +280,30 @@ def get_folio_payments(self, folio_id): ) ) return payments + + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.reservation.info", is_list=False), + auth="jwt_api_pms", + ) + def create_reservation(self, pms_reservation_info): + reservation = self.env["pms.reservation"].sudo().create( + { + "partner_name": pms_reservation_info.partner, + "pms_property_id": pms_reservation_info.property, + "room_type_id": pms_reservation_info.roomTypeId, + "pricelist_id": pms_reservation_info.pricelistId, + "checkin": pms_reservation_info.checkin, + "checkout": pms_reservation_info.checkout, + "board_service_room_id": pms_reservation_info.boardServiceId, + "channel_type_id": pms_reservation_info.channelTypeId, + } + ) + return reservation.id From 57ad92920f5badf47191f2c57d818a5b6c45eee2 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Jan 2022 12:28:29 +0100 Subject: [PATCH 042/547] [IMP] pms_api_rest: filter room service by property --- pms_api_rest/datamodels/pms_room_search_param.py | 1 + pms_api_rest/services/room_services.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_room_search_param.py b/pms_api_rest/datamodels/pms_room_search_param.py index 3ef72c8ae7..1ba09dc0eb 100644 --- a/pms_api_rest/datamodels/pms_room_search_param.py +++ b/pms_api_rest/datamodels/pms_room_search_param.py @@ -7,3 +7,4 @@ class PmsRoomSearchParam(Datamodel): _name = "pms.room.search.param" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) + pms_property_id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/room_services.py index 8bc61e2152..22985d823c 100644 --- a/pms_api_rest/services/room_services.py +++ b/pms_api_rest/services/room_services.py @@ -28,6 +28,9 @@ def get_rooms(self, room_search_param): domain.append(("name", "like", room_search_param.name)) if room_search_param.id: domain.append(("id", "=", room_search_param.id)) + if room_search_param.pms_property_id: + domain.append(("pms_property_id", "=", room_search_param.pms_property_id)) + result_rooms = [] PmsRoomInfo = self.env.datamodels["pms.room.info"] for room in ( From d91868783b66b609d5c8bc7037b9850f5a940019 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Jan 2022 12:35:23 +0100 Subject: [PATCH 043/547] [FIX] pms_api_rest: fix precommit --- pms_api_rest/services/folio_services.py | 26 ++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index 251cc06f65..e5c03dbc0f 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -294,16 +294,20 @@ def get_folio_payments(self, folio_id): auth="jwt_api_pms", ) def create_reservation(self, pms_reservation_info): - reservation = self.env["pms.reservation"].sudo().create( - { - "partner_name": pms_reservation_info.partner, - "pms_property_id": pms_reservation_info.property, - "room_type_id": pms_reservation_info.roomTypeId, - "pricelist_id": pms_reservation_info.pricelistId, - "checkin": pms_reservation_info.checkin, - "checkout": pms_reservation_info.checkout, - "board_service_room_id": pms_reservation_info.boardServiceId, - "channel_type_id": pms_reservation_info.channelTypeId, - } + reservation = ( + self.env["pms.reservation"] + .sudo() + .create( + { + "partner_name": pms_reservation_info.partner, + "pms_property_id": pms_reservation_info.property, + "room_type_id": pms_reservation_info.roomTypeId, + "pricelist_id": pms_reservation_info.pricelistId, + "checkin": pms_reservation_info.checkin, + "checkout": pms_reservation_info.checkout, + "board_service_room_id": pms_reservation_info.boardServiceId, + "channel_type_id": pms_reservation_info.channelTypeId, + } + ) ) return reservation.id From 56578d44d84c789503f3e527bf18f830050a52af Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Jan 2022 12:36:59 +0100 Subject: [PATCH 044/547] [IMP] pms_api_rest: pricelist $ avail. plan service" --- pms_api_rest/datamodels/__init__.py | 4 + pms_api_rest/datamodels/pms_pricelist_info.py | 10 ++ .../datamodels/pms_pricelist_item_info.py | 21 +++ .../pms_pricelist_item_search_param.py | 10 ++ pms_api_rest/services/__init__.py | 4 +- pms_api_rest/services/pricelist_service.py | 142 ++++++++++++++++++ 6 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_pricelist_info.py create mode 100644 pms_api_rest/datamodels/pms_pricelist_item_info.py create mode 100644 pms_api_rest/datamodels/pms_pricelist_item_search_param.py create mode 100644 pms_api_rest/services/pricelist_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 248ce06e53..734e3fee03 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -22,3 +22,7 @@ from . import pms_payment_info from . import user_input from . import user_output + +from . import pms_pricelist_info +from . import pms_pricelist_item_search_param +from . import pms_pricelist_item_info diff --git a/pms_api_rest/datamodels/pms_pricelist_info.py b/pms_api_rest/datamodels/pms_pricelist_info.py new file mode 100644 index 0000000000..6f2fd1d433 --- /dev/null +++ b/pms_api_rest/datamodels/pms_pricelist_info.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPricelistInfo(Datamodel): + _name = "pms.pricelist.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + pms_property_id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_pricelist_item_info.py b/pms_api_rest/datamodels/pms_pricelist_item_info.py new file mode 100644 index 0000000000..e52986bf85 --- /dev/null +++ b/pms_api_rest/datamodels/pms_pricelist_item_info.py @@ -0,0 +1,21 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPricelistItemInfo(Datamodel): + _name = "pms.pricelist.item.info" + pricelist_item_id = fields.Integer(required=False, allow_none=True) + availability_rule_id = fields.Integer(required=False, allow_none=True) + fixed_price = fields.Float(required=False, allow_none=True) + min_stay = fields.Integer(required=False, allow_none=True) + min_stay_arrival = fields.Integer(required=False, allow_none=True) + max_stay = fields.Integer(required=False, allow_none=True) + max_stay_arrival = fields.Integer(required=False, allow_none=True) + closed = fields.Boolean(required=False, allow_none=True) + closed_departure = fields.Boolean(required=False, allow_none=True) + closed_arrival = fields.Boolean(required=False, allow_none=True) + quota = fields.Integer(required=False, allow_none=True) + max_avail = fields.Integer(required=False, allow_none=True) + room_type_id = fields.Integer(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_pricelist_item_search_param.py b/pms_api_rest/datamodels/pms_pricelist_item_search_param.py new file mode 100644 index 0000000000..33b4bafcc7 --- /dev/null +++ b/pms_api_rest/datamodels/pms_pricelist_item_search_param.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPricelistItemSearchParam(Datamodel): + _name = "pms.pricelist.item.search.param" + date_from = fields.String(required=True, allow_none=False) + date_to = fields.String(required=True, allow_none=False) + pms_property_id = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 064c89aae5..706570938c 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -5,8 +5,6 @@ from . import partner_services from . import reservation_services -<<<<<<< HEAD from . import property_services -======= from . import login_service ->>>>>>> a4394db3... [REF] pms-api-rest: add controller 4 login +from . import pricelist_service diff --git a/pms_api_rest/services/pricelist_service.py b/pms_api_rest/services/pricelist_service.py new file mode 100644 index 0000000000..b0e4785b43 --- /dev/null +++ b/pms_api_rest/services/pricelist_service.py @@ -0,0 +1,142 @@ +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 PmsPricelistService(Component): + _inherit = "base.rest.service" + _name = "pms.pricelist.service" + _usage = "pricelists" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.pricelist.info", is_list=False), + output_param=Datamodel("pms.pricelist.info", is_list=True), + auth="jwt_api_pms", + ) + def get_pricelists(self, pricelist_info_search_param, **args): + domain = [] + if pricelist_info_search_param.pms_property_id: + domain.append( + ( + "pms_property_ids", + "in", + [pricelist_info_search_param.pms_property_id], + ) + ) + PmsPricelistInfo = self.env.datamodels["pms.pricelist.info"] + result_pricelists = [] + for pricelist in self.env["product.pricelist"].sudo().search(domain): + result_pricelists.append( + PmsPricelistInfo( + id=pricelist.id, + name=pricelist.name, + ) + ) + return result_pricelists + + @restapi.method( + [ + ( + [ + "/", + ], + "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): + result = [] + record_pricelist_id = ( + self.env["product.pricelist"].sudo().search([("id", "=", pricelist_id)]) + ) + if not record_pricelist_id: + raise MissingError + PmsPricelistItemInfo = self.env.datamodels["pms.pricelist.item.info"] + rooms = ( + self.env["pms.room"] + .sudo() + .search( + [("pms_property_id", "=", pricelist_item_search_param.pms_property_id)] + ) + ) + for room_type in ( + self.env["pms.room.type"] + .sudo() + .search([("id", "in", rooms.mapped("room_type_id").ids)]) + ): + for item in ( + self.env["product.pricelist.item"] + .sudo() + .search( + [ + ("pricelist_id", "=", pricelist_id), + ("applied_on", "=", "0_product_variant"), + ("product_id", "=", room_type.product_id.id), + ( + "date_start_consumption", + ">=", + pricelist_item_search_param.date_from, + ), + ( + "date_end_consumption", + "<=", + pricelist_item_search_param.date_to, + ), + ] + ) + ): + rule = ( + self.env["pms.availability.plan.rule"] + .sudo() + .search( + [ + ( + "availability_plan_id", + "=", + record_pricelist_id.availability_plan_id.id, + ), + ("date", "=", item.date_start_consumption), + ("date", "=", item.date_end_consumption), + ("room_type_id", "=", room_type.id), + ( + "pms_property_id", + "=", + pricelist_item_search_param.pms_property_id, + ), + ] + ) + ) + rule.ensure_one() + result.append( + PmsPricelistItemInfo( + pricelist_item_id=item.id, + availability_rule_id=rule.id, + room_type_id=room_type.id, + fixed_price=item.fixed_price, + min_stay=rule.min_stay, + min_stay_arrival=rule.min_stay_arrival, + max_stay=rule.max_stay, + max_stay_arrival=rule.max_stay_arrival, + closed=rule.closed, + closed_departure=rule.closed_departure, + closed_arrival=rule.closed_arrival, + quota=rule.quota, + max_avail=rule.max_avail, + date=str(item.date_start_consumption), + ) + ) + return result From 5c269de9a41b5ec2745ee7d686ed963af1532f77 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Jan 2022 12:40:07 +0100 Subject: [PATCH 045/547] [DEL] pms_api_rest: remove zip @ static other module --- .../static/Models&ReservationCajonv2xD.zip | Bin 47271 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pms_l10n_es/static/Models&ReservationCajonv2xD.zip diff --git a/pms_l10n_es/static/Models&ReservationCajonv2xD.zip b/pms_l10n_es/static/Models&ReservationCajonv2xD.zip deleted file mode 100644 index a322610516e92187754ebd4e147a59d8c38a2b90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47271 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 z 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 R|JVommrDPYmu>%P{TGeQ^f~|l From 9b4c2d4c9e5bdd0d0c32329ed907dd625b70ee9e Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Jan 2022 13:01:45 +0100 Subject: [PATCH 046/547] [IMP] pms_api_rest: not using session in api rest --- pms_api_rest/controllers/pms_rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/controllers/pms_rest.py b/pms_api_rest/controllers/pms_rest.py index cb3481c411..366984bbe3 100644 --- a/pms_api_rest/controllers/pms_rest.py +++ b/pms_api_rest/controllers/pms_rest.py @@ -5,5 +5,5 @@ class BaseRestPrivateApiController(main.RestController): _root_path = "/api/" _collection_name = "pms.services" _default_auth = "public" - + _default_save_session = False _default_cors = "*" From e42e4ebb472391b2b728d8cf29bd51ef5cc99735 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 24 Jan 2022 16:18:24 +0100 Subject: [PATCH 047/547] [REF] pms_api_rest: refactor folio_services --- .../datamodels/pms_reservation_info.py | 5 +- pms_api_rest/services/folio_services.py | 119 -------------- pms_api_rest/services/reservation_services.py | 147 +++++++++++++++--- 3 files changed, 130 insertions(+), 141 deletions(-) diff --git a/pms_api_rest/datamodels/pms_reservation_info.py b/pms_api_rest/datamodels/pms_reservation_info.py index ade4d72262..08f7bfffbc 100644 --- a/pms_api_rest/datamodels/pms_reservation_info.py +++ b/pms_api_rest/datamodels/pms_reservation_info.py @@ -12,7 +12,8 @@ class PmsReservationInfo(Datamodel): checkout = fields.String(required=False, allow_none=True) roomTypeId = fields.Integer(required=False, allow_none=True) roomTypeName = fields.String(required=False, allow_none=True) - preferredRoomId = fields.String(required=False, allow_none=True) + preferredRoomName = fields.String(required=False, allow_none=True) + preferredRoomId = fields.Integer(required=False, allow_none=True) priceTotal = fields.Float(required=False, allow_none=True) priceOnlyServices = fields.Float(required=False, allow_none=True) priceOnlyRoom = fields.Float(required=False, allow_none=True) @@ -22,4 +23,6 @@ class PmsReservationInfo(Datamodel): messages = fields.List(fields.Dict(required=False, allow_none=True)) property = fields.Integer(required=False, allow_none=True) boardServiceId = fields.Integer(required=False, allow_none=True) + boardServiceName = fields.String(required=False, allow_none=True) channelTypeId = fields.Integer(required=False, allow_none=True) + adults = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/folio_services.py index e5c03dbc0f..8b1054b9a6 100644 --- a/pms_api_rest/services/folio_services.py +++ b/pms_api_rest/services/folio_services.py @@ -114,125 +114,6 @@ def get_folios(self, folio_search_param): ) return result_folios - @restapi.method( - [ - ( - [ - "//reservations/", - ], - "GET", - ) - ], - output_param=Datamodel("pms.reservation.info"), - auth="jwt_api_pms", - ) - def get_reservation(self, folio_id, reservation_id): - reservation = ( - self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) - ) - res = [] - PmsReservationInfo = self.env.datamodels["pms.reservation.info"] - if not reservation: - pass - else: - services = [] - for service in reservation.service_ids: - if service.is_board_service: - services.append( - { - "id": service.id, - "name": service.name, - "quantity": service.product_qty, - "priceTotal": service.price_total, - "priceSubtotal": service.price_subtotal, - "priceTaxes": service.price_tax, - "discount": service.discount, - } - ) - 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, - partner=reservation.partner_id.name, - checkin=str(reservation.checkin), - checkout=str(reservation.checkout), - preferredRoomId=reservation.preferred_room_id.name - if reservation.preferred_room_id - else "", - roomTypeName=reservation.room_type_id.name - if reservation.room_type_id - else "", - name=reservation.name, - priceTotal=reservation.price_room_services_set, - priceOnlyServices=reservation.price_services - if reservation.price_services - else 0.0, - priceOnlyRoom=reservation.price_total, - pricelistName=reservation.pricelist_id.name - if reservation.pricelist_id - else "", - services=services if services else [], - messages=messages, - ) - return res - - @restapi.method( - [ - ( - [ - "//reservations//checkinpartners", - ], - "GET", - ) - ], - output_param=Datamodel("pms.checkin.partner.info", is_list=True), - auth="jwt_api_pms", - ) - def get_checkin_partners(self, folio_id, reservation_id): - reservation = ( - self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) - ) - checkin_partners = [] - PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] - if not reservation: - pass - else: - for checkin_partner in reservation.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 "", - email=checkin_partner.email if checkin_partner.email else "", - mobile=checkin_partner.mobile if checkin_partner.mobile else "", - nationality=checkin_partner.nationality_id.name - if checkin_partner.nationality_id - else "", - documentType=checkin_partner.document_type.name - if checkin_partner.document_type - else "", - documentNumber=checkin_partner.document_number - if checkin_partner.document_number - else "", - gender=checkin_partner.gender if checkin_partner.gender else "", - state=dict( - checkin_partner.fields_get(["state"])["state"]["selection"] - )[checkin_partner.state], - ) - ) - return checkin_partners - @restapi.method( [ ( diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/reservation_services.py index 96fc4da35a..af2582c637 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/reservation_services.py @@ -15,35 +15,94 @@ class PmsRoomService(Component): [ ( [ - "/", + "/", ], "GET", ) ], - output_param=Datamodel("pms.reservation.info", is_list=True), + output_param=Datamodel("pms.reservation.info"), auth="jwt_api_pms", ) - def get_reservations(self): - domain = [] + def get_reservation(self, reservation_id): + reservation = ( + self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) + ) + res = [] + PmsReservationInfo = self.env.datamodels["pms.reservation.info"] + if not reservation: + pass + else: + services = [] + for service in reservation.service_ids: + if service.is_board_service: + services.append( + { + "id": service.id, + "name": service.name, + "quantity": service.product_qty, + "priceTotal": service.price_total, + "priceSubtotal": service.price_subtotal, + "priceTaxes": service.price_tax, + "discount": service.discount, + } + ) + messages = [] + import re - result_reservations = [] - PmsReservationInfo = self.env.datamodels["pms.reservation..info"] - for reservation in ( - self.env["pms.reservation"] - .sudo() - .search( - domain, - ) - ): - result_reservations.append( - PmsReservationInfo( - id=reservation.id, - price=reservation.price_subtotal, - checkin=datetime.combine(reservation.checkin, datetime.min.time()).isoformat(), - checkout=datetime.combine(reservation.checkout, datetime.min.time()).isoformat(), + 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, + partner=reservation.partner_id.name, + checkin=str(reservation.checkin), + checkout=str(reservation.checkout), + preferredRoomId=reservation.preferred_room_id.id + if reservation.preferred_room_id + else 0, + preferredRoomName=reservation.preferred_room_id.name + if reservation.preferred_room_id + else "", + roomTypeId=reservation.room_type_id.id + if reservation.room_type_id + else 0, + roomTypeName=reservation.room_type_id.name + if reservation.room_type_id + else "", + name=reservation.name, + priceTotal=reservation.price_room_services_set, + priceOnlyServices=reservation.price_services + if reservation.price_services + else 0.0, + priceOnlyRoom=reservation.price_total, + pricelistName=reservation.pricelist_id.name + if reservation.pricelist_id + else "", + pricelistId=reservation.pricelist_id.id + if reservation.pricelist_id + else 0, + services=services if services else [], + messages=messages, + boardServiceId=reservation.board_service_room_id.id + if reservation.board_service_room_id + else 0, + boardServiceName=reservation.board_service_room_id.pms_board_service_id.name + if reservation.board_service_room_id + else "", + # review if its an agency + channelTypeId=reservation.channel_type_id.id + if reservation.channel_type_id + else 0, + adults=reservation.adults, ) - return result_reservations + return res @restapi.method( [ @@ -57,7 +116,7 @@ def get_reservations(self): input_param=Datamodel("pms.calendar.changes", is_list=False), auth="jwt_api_pms", ) - def move_reservation_line(self, reservation_id, reservation_lines_changes): + def update_reservation(self, reservation_id, reservation_lines_changes): # get date of first reservation id to change first_reservation_line_id_to_change = ( @@ -108,3 +167,49 @@ def move_reservation_line(self, reservation_id, reservation_lines_changes): reservation = self.env["pms.reservation"].browse(reservation_id) reservation.checkin = min_value reservation.checkout = max_value + + @restapi.method( + [ + ( + [ + "//checkinpartners", + ], + "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"].sudo().search([("id", "=", reservation_id)]) + ) + checkin_partners = [] + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] + if not reservation: + pass + else: + for checkin_partner in reservation.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 "", + email=checkin_partner.email if checkin_partner.email else "", + mobile=checkin_partner.mobile if checkin_partner.mobile else "", + nationality=checkin_partner.nationality_id.name + if checkin_partner.nationality_id + else "", + documentType=checkin_partner.document_type.name + if checkin_partner.document_type + else "", + documentNumber=checkin_partner.document_number + if checkin_partner.document_number + else "", + gender=checkin_partner.gender if checkin_partner.gender else "", + state=dict( + checkin_partner.fields_get(["state"])["state"]["selection"] + )[checkin_partner.state], + ) + ) + return checkin_partners From fcdfd96d78112e11ec17312c2d84476c784df59c Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 25 Jan 2022 13:01:08 +0100 Subject: [PATCH 048/547] [REF] pms_api_rest: consistency in datamodels and services filenames --- __init__.py | 0 pms_api_rest/datamodels/__init__.py | 34 +++++++------------ ...journal_info.py => pms_account_journal.py} | 0 .../{pms_calendar_info.py => pms_calendar.py} | 19 +++++++++++ .../datamodels/pms_calendar_changes.py | 8 ----- .../datamodels/pms_calendar_search_param.py | 9 ----- .../datamodels/pms_calendar_swap_info.py | 11 ------ ...partner_info.py => pms_checkin_partner.py} | 0 .../{pms_folio_info.py => pms_folio.py} | 7 ++++ .../datamodels/pms_folio_search_param.py | 10 ------ .../{pms_partner_info.py => pms_partner.py} | 0 .../{pms_payment_info.py => pms_payment.py} | 0 ...pms_pricelist_info.py => pms_pricelist.py} | 0 ...ist_item_info.py => pms_pricelist_item.py} | 7 ++++ .../pms_pricelist_item_search_param.py | 10 ------ .../{pms_property_info.py => pms_property.py} | 7 ++++ .../datamodels/pms_property_search_param.py | 10 ------ ...reservation_info.py => pms_reservation.py} | 0 .../{pms_room_info.py => pms_room.py} | 7 ++++ .../datamodels/pms_room_search_param.py | 10 ------ ..._type_search_param.py => pms_room_type.py} | 6 ++++ pms_api_rest/datamodels/pms_room_type_info.py | 9 ----- .../{user_output.py => pms_user.py} | 6 ++++ pms_api_rest/datamodels/user_input.py | 9 ----- pms_api_rest/services/__init__.py | 18 +++++----- ...dar_service.py => pms_calendar_service.py} | 0 ...folio_services.py => pms_folio_service.py} | 0 ...{login_service.py => pms_login_service.py} | 2 +- ...ner_services.py => pms_partner_service.py} | 0 ...st_service.py => pms_pricelist_service.py} | 0 ...ty_services.py => pms_property_service.py} | 2 +- ...services.py => pms_reservation_service.py} | 2 +- .../{room_services.py => pms_room_service.py} | 0 ..._services.py => pms_room_type_services.py} | 0 34 files changed, 84 insertions(+), 119 deletions(-) create mode 100644 __init__.py rename pms_api_rest/datamodels/{pms_account_journal_info.py => pms_account_journal.py} (100%) rename pms_api_rest/datamodels/{pms_calendar_info.py => pms_calendar.py} (65%) delete mode 100644 pms_api_rest/datamodels/pms_calendar_changes.py delete mode 100644 pms_api_rest/datamodels/pms_calendar_search_param.py delete mode 100644 pms_api_rest/datamodels/pms_calendar_swap_info.py rename pms_api_rest/datamodels/{pms_checkin_partner_info.py => pms_checkin_partner.py} (100%) rename pms_api_rest/datamodels/{pms_folio_info.py => pms_folio.py} (82%) delete mode 100644 pms_api_rest/datamodels/pms_folio_search_param.py rename pms_api_rest/datamodels/{pms_partner_info.py => pms_partner.py} (100%) rename pms_api_rest/datamodels/{pms_payment_info.py => pms_payment.py} (100%) rename pms_api_rest/datamodels/{pms_pricelist_info.py => pms_pricelist.py} (100%) rename pms_api_rest/datamodels/{pms_pricelist_item_info.py => pms_pricelist_item.py} (79%) delete mode 100644 pms_api_rest/datamodels/pms_pricelist_item_search_param.py rename pms_api_rest/datamodels/{pms_property_info.py => pms_property.py} (61%) delete mode 100644 pms_api_rest/datamodels/pms_property_search_param.py rename pms_api_rest/datamodels/{pms_reservation_info.py => pms_reservation.py} (100%) rename pms_api_rest/datamodels/{pms_room_info.py => pms_room.py} (59%) delete mode 100644 pms_api_rest/datamodels/pms_room_search_param.py rename pms_api_rest/datamodels/{pms_room_type_search_param.py => pms_room_type.py} (60%) delete mode 100644 pms_api_rest/datamodels/pms_room_type_info.py rename pms_api_rest/datamodels/{user_output.py => pms_user.py} (62%) delete mode 100644 pms_api_rest/datamodels/user_input.py rename pms_api_rest/services/{calendar_service.py => pms_calendar_service.py} (100%) rename pms_api_rest/services/{folio_services.py => pms_folio_service.py} (100%) rename pms_api_rest/services/{login_service.py => pms_login_service.py} (97%) rename pms_api_rest/services/{partner_services.py => pms_partner_service.py} (100%) rename pms_api_rest/services/{pricelist_service.py => pms_pricelist_service.py} (100%) rename pms_api_rest/services/{property_services.py => pms_property_service.py} (98%) rename pms_api_rest/services/{reservation_services.py => pms_reservation_service.py} (99%) rename pms_api_rest/services/{room_services.py => pms_room_service.py} (100%) rename pms_api_rest/services/{room_type_services.py => pms_room_type_services.py} (100%) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 734e3fee03..01b14bdff1 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -1,28 +1,20 @@ -from . import pms_calendar_search_param -from . import pms_calendar_info +from . import pms_calendar -from . import pms_folio_search_param -from . import pms_folio_info +from . import pms_folio -from . import pms_room_info -from . import pms_room_type_info -from . import pms_room_search_param +from . import pms_room +from . import pms_room_type -from . import pms_reservation_info +from . import pms_reservation -from . import pms_checkin_partner_info -from . import pms_partner_info +from . import pms_checkin_partner +from . import pms_partner -from . import pms_calendar_swap_info -from . import pms_calendar_changes +from . import pms_property +from . import pms_account_journal +from . import pms_payment -from . import pms_property_info -from . import pms_property_search_param -from . import pms_account_journal_info -from . import pms_payment_info -from . import user_input -from . import user_output +from . import pms_user -from . import pms_pricelist_info -from . import pms_pricelist_item_search_param -from . import pms_pricelist_item_info +from . import pms_pricelist +from . import pms_pricelist_item diff --git a/pms_api_rest/datamodels/pms_account_journal_info.py b/pms_api_rest/datamodels/pms_account_journal.py similarity index 100% rename from pms_api_rest/datamodels/pms_account_journal_info.py rename to pms_api_rest/datamodels/pms_account_journal.py diff --git a/pms_api_rest/datamodels/pms_calendar_info.py b/pms_api_rest/datamodels/pms_calendar.py similarity index 65% rename from pms_api_rest/datamodels/pms_calendar_info.py rename to pms_api_rest/datamodels/pms_calendar.py index f5ccf56cf1..cffc2569a1 100644 --- a/pms_api_rest/datamodels/pms_calendar_info.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -3,6 +3,25 @@ from odoo.addons.datamodel.core import Datamodel +class PmsCalendarChanges(Datamodel): + _name = "pms.calendar.changes" + reservationLinesChanges = fields.List(fields.Dict(required=False, allow_none=True)) + + +class PmsCalendarSwapInfo(Datamodel): + _name = "pms.calendar.swap.info" + swapFrom = fields.String(required=True, allow_none=False) + swapTo = fields.String(required=True, allow_none=False) + roomIdA = fields.Integer(required=True, allow_none=False) + roomIdB = fields.Integer(required=True, allow_none=False) + + +class PmsCalendarSearchParam(Datamodel): + _name = "pms.calendar.search.param" + date_from = fields.String(required=False, allow_none=True) + date_to = fields.String(required=False, allow_none=True) + + class PmsCalendarInfo(Datamodel): _name = "pms.calendar.info" id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_calendar_changes.py b/pms_api_rest/datamodels/pms_calendar_changes.py deleted file mode 100644 index 77e3e70747..0000000000 --- a/pms_api_rest/datamodels/pms_calendar_changes.py +++ /dev/null @@ -1,8 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsCalendarChanges(Datamodel): - _name = "pms.calendar.changes" - reservationLinesChanges = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/datamodels/pms_calendar_search_param.py b/pms_api_rest/datamodels/pms_calendar_search_param.py deleted file mode 100644 index 1414c55ecc..0000000000 --- a/pms_api_rest/datamodels/pms_calendar_search_param.py +++ /dev/null @@ -1,9 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsCalendarSearchParam(Datamodel): - _name = "pms.calendar.search.param" - date_from = fields.String(required=False, allow_none=True) - date_to = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_calendar_swap_info.py b/pms_api_rest/datamodels/pms_calendar_swap_info.py deleted file mode 100644 index ee9f2a0bb0..0000000000 --- a/pms_api_rest/datamodels/pms_calendar_swap_info.py +++ /dev/null @@ -1,11 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsCalendarSwapInfo(Datamodel): - _name = "pms.calendar.swap.info" - swapFrom = fields.String(required=True, allow_none=False) - swapTo = fields.String(required=True, allow_none=False) - roomIdA = fields.Integer(required=True, allow_none=False) - roomIdB = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_checkin_partner_info.py b/pms_api_rest/datamodels/pms_checkin_partner.py similarity index 100% rename from pms_api_rest/datamodels/pms_checkin_partner_info.py rename to pms_api_rest/datamodels/pms_checkin_partner.py diff --git a/pms_api_rest/datamodels/pms_folio_info.py b/pms_api_rest/datamodels/pms_folio.py similarity index 82% rename from pms_api_rest/datamodels/pms_folio_info.py rename to pms_api_rest/datamodels/pms_folio.py index 54fe504b10..da9febb5ba 100644 --- a/pms_api_rest/datamodels/pms_folio_info.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -3,6 +3,13 @@ from odoo.addons.datamodel.core import Datamodel +class PmsFolioSearchParam(Datamodel): + _name = "pms.folio.search.param" + + date_from = fields.String(required=False, allow_none=True) + date_to = fields.String(required=False, allow_none=True) + + class PmsFolioInfo(Datamodel): _name = "pms.folio.info" id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_folio_search_param.py b/pms_api_rest/datamodels/pms_folio_search_param.py deleted file mode 100644 index 5a37c60034..0000000000 --- a/pms_api_rest/datamodels/pms_folio_search_param.py +++ /dev/null @@ -1,10 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsFolioSearchParam(Datamodel): - _name = "pms.folio.search.param" - - date_from = fields.String(required=False, allow_none=True) - date_to = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_partner_info.py b/pms_api_rest/datamodels/pms_partner.py similarity index 100% rename from pms_api_rest/datamodels/pms_partner_info.py rename to pms_api_rest/datamodels/pms_partner.py diff --git a/pms_api_rest/datamodels/pms_payment_info.py b/pms_api_rest/datamodels/pms_payment.py similarity index 100% rename from pms_api_rest/datamodels/pms_payment_info.py rename to pms_api_rest/datamodels/pms_payment.py diff --git a/pms_api_rest/datamodels/pms_pricelist_info.py b/pms_api_rest/datamodels/pms_pricelist.py similarity index 100% rename from pms_api_rest/datamodels/pms_pricelist_info.py rename to pms_api_rest/datamodels/pms_pricelist.py diff --git a/pms_api_rest/datamodels/pms_pricelist_item_info.py b/pms_api_rest/datamodels/pms_pricelist_item.py similarity index 79% rename from pms_api_rest/datamodels/pms_pricelist_item_info.py rename to pms_api_rest/datamodels/pms_pricelist_item.py index e52986bf85..8ec3c285f0 100644 --- a/pms_api_rest/datamodels/pms_pricelist_item_info.py +++ b/pms_api_rest/datamodels/pms_pricelist_item.py @@ -3,6 +3,13 @@ from odoo.addons.datamodel.core import Datamodel +class PmsPricelistItemSearchParam(Datamodel): + _name = "pms.pricelist.item.search.param" + date_from = fields.String(required=True, allow_none=False) + date_to = fields.String(required=True, allow_none=False) + pms_property_id = fields.Integer(required=True, allow_none=False) + + class PmsPricelistItemInfo(Datamodel): _name = "pms.pricelist.item.info" pricelist_item_id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_pricelist_item_search_param.py b/pms_api_rest/datamodels/pms_pricelist_item_search_param.py deleted file mode 100644 index 33b4bafcc7..0000000000 --- a/pms_api_rest/datamodels/pms_pricelist_item_search_param.py +++ /dev/null @@ -1,10 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsPricelistItemSearchParam(Datamodel): - _name = "pms.pricelist.item.search.param" - date_from = fields.String(required=True, allow_none=False) - date_to = fields.String(required=True, allow_none=False) - pms_property_id = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_property_info.py b/pms_api_rest/datamodels/pms_property.py similarity index 61% rename from pms_api_rest/datamodels/pms_property_info.py rename to pms_api_rest/datamodels/pms_property.py index 79db922407..6a10fa855b 100644 --- a/pms_api_rest/datamodels/pms_property_info.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -3,6 +3,13 @@ from odoo.addons.datamodel.core import Datamodel +class PmsPropertySearchParam(Datamodel): + _name = "pms.property.search.param" + + id = fields.Integer(required=False, allow_none=False) + name = fields.String(required=False, allow_none=False) + + class PmsPropertyInfo(Datamodel): _name = "pms.property.info" id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_property_search_param.py b/pms_api_rest/datamodels/pms_property_search_param.py deleted file mode 100644 index c8a1f81b46..0000000000 --- a/pms_api_rest/datamodels/pms_property_search_param.py +++ /dev/null @@ -1,10 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsPropertySearchParam(Datamodel): - _name = "pms.property.search.param" - - id = fields.Integer(required=False, allow_none=False) - name = fields.String(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_reservation_info.py b/pms_api_rest/datamodels/pms_reservation.py similarity index 100% rename from pms_api_rest/datamodels/pms_reservation_info.py rename to pms_api_rest/datamodels/pms_reservation.py diff --git a/pms_api_rest/datamodels/pms_room_info.py b/pms_api_rest/datamodels/pms_room.py similarity index 59% rename from pms_api_rest/datamodels/pms_room_info.py rename to pms_api_rest/datamodels/pms_room.py index d6a2603fb3..61825ea491 100644 --- a/pms_api_rest/datamodels/pms_room_info.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -3,6 +3,13 @@ from odoo.addons.datamodel.core import Datamodel +class PmsRoomSearchParam(Datamodel): + _name = "pms.room.search.param" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + pms_property_id = fields.Integer(required=False, allow_none=True) + + class PmsRoomInfo(Datamodel): _name = "pms.room.info" id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room_search_param.py b/pms_api_rest/datamodels/pms_room_search_param.py deleted file mode 100644 index 1ba09dc0eb..0000000000 --- a/pms_api_rest/datamodels/pms_room_search_param.py +++ /dev/null @@ -1,10 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsRoomSearchParam(Datamodel): - _name = "pms.room.search.param" - id = fields.Integer(required=False, allow_none=True) - name = fields.String(required=False, allow_none=True) - pms_property_id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room_type_search_param.py b/pms_api_rest/datamodels/pms_room_type.py similarity index 60% rename from pms_api_rest/datamodels/pms_room_type_search_param.py rename to pms_api_rest/datamodels/pms_room_type.py index 3ee1780f1a..c1bd369dc5 100644 --- a/pms_api_rest/datamodels/pms_room_type_search_param.py +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -7,3 +7,9 @@ class PmsRoomTypeSearchParam(Datamodel): _name = "pms.room.type.search.param" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) + + +class PmsRoomTypeInfo(Datamodel): + _name = "pms.room.type.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_room_type_info.py b/pms_api_rest/datamodels/pms_room_type_info.py deleted file mode 100644 index ce3ad3ea94..0000000000 --- a/pms_api_rest/datamodels/pms_room_type_info.py +++ /dev/null @@ -1,9 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsRoomTypeInfo(Datamodel): - _name = "pms.room.type.info" - id = fields.Integer(required=False, allow_none=True) - name = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/user_output.py b/pms_api_rest/datamodels/pms_user.py similarity index 62% rename from pms_api_rest/datamodels/user_output.py rename to pms_api_rest/datamodels/pms_user.py index ee17fe5cac..83b03cef3f 100644 --- a/pms_api_rest/datamodels/user_output.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -3,6 +3,12 @@ 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) + + class PmsApiRestUserOutput(Datamodel): _name = "pms.api.rest.user.output" # user = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/user_input.py b/pms_api_rest/datamodels/user_input.py deleted file mode 100644 index f370c2a609..0000000000 --- a/pms_api_rest/datamodels/user_input.py +++ /dev/null @@ -1,9 +0,0 @@ -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) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 706570938c..f6249f90a5 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -1,10 +1,10 @@ -from . import folio_services -from . import room_services -from . import room_type_services -from . import calendar_service -from . import partner_services +from . import pms_folio_service +from . import pms_room_service +from . import pms_room_type_services +from . import pms_calendar_service +from . import pms_partner_service -from . import reservation_services -from . import property_services -from . import login_service -from . import pricelist_service +from . import pms_reservation_service +from . import pms_property_service +from . import pms_login_service +from . import pms_pricelist_service diff --git a/pms_api_rest/services/calendar_service.py b/pms_api_rest/services/pms_calendar_service.py similarity index 100% rename from pms_api_rest/services/calendar_service.py rename to pms_api_rest/services/pms_calendar_service.py diff --git a/pms_api_rest/services/folio_services.py b/pms_api_rest/services/pms_folio_service.py similarity index 100% rename from pms_api_rest/services/folio_services.py rename to pms_api_rest/services/pms_folio_service.py diff --git a/pms_api_rest/services/login_service.py b/pms_api_rest/services/pms_login_service.py similarity index 97% rename from pms_api_rest/services/login_service.py rename to pms_api_rest/services/pms_login_service.py index ac45cc993d..a719cc1879 100644 --- a/pms_api_rest/services/login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -10,7 +10,7 @@ from odoo.addons.component.core import Component -class PmsPartnerService(Component): +class PmsLoginService(Component): _inherit = "base.rest.service" _name = "pms.auth.service" _usage = "login" diff --git a/pms_api_rest/services/partner_services.py b/pms_api_rest/services/pms_partner_service.py similarity index 100% rename from pms_api_rest/services/partner_services.py rename to pms_api_rest/services/pms_partner_service.py diff --git a/pms_api_rest/services/pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py similarity index 100% rename from pms_api_rest/services/pricelist_service.py rename to pms_api_rest/services/pms_pricelist_service.py diff --git a/pms_api_rest/services/property_services.py b/pms_api_rest/services/pms_property_service.py similarity index 98% rename from pms_api_rest/services/property_services.py rename to pms_api_rest/services/pms_property_service.py index e5573a61f4..c5ce496b97 100644 --- a/pms_api_rest/services/property_services.py +++ b/pms_api_rest/services/pms_property_service.py @@ -3,7 +3,7 @@ from odoo.addons.component.core import Component -class PmsPropertyComponent(Component): +class PmsPropertyService(Component): _inherit = "base.rest.service" _name = "pms.property.service" _usage = "properties" diff --git a/pms_api_rest/services/reservation_services.py b/pms_api_rest/services/pms_reservation_service.py similarity index 99% rename from pms_api_rest/services/reservation_services.py rename to pms_api_rest/services/pms_reservation_service.py index af2582c637..972e8129a7 100644 --- a/pms_api_rest/services/reservation_services.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -5,7 +5,7 @@ from odoo.addons.component.core import Component -class PmsRoomService(Component): +class PmsReservationService(Component): _inherit = "base.rest.service" _name = "pms.reservation.service" _usage = "reservations" diff --git a/pms_api_rest/services/room_services.py b/pms_api_rest/services/pms_room_service.py similarity index 100% rename from pms_api_rest/services/room_services.py rename to pms_api_rest/services/pms_room_service.py diff --git a/pms_api_rest/services/room_type_services.py b/pms_api_rest/services/pms_room_type_services.py similarity index 100% rename from pms_api_rest/services/room_type_services.py rename to pms_api_rest/services/pms_room_type_services.py From 8aa066481d04c1acdeb7d0af4fd05f9e40bc4632 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 27 Jan 2022 08:26:58 +0000 Subject: [PATCH 049/547] [FIX] pms_api_rest: remove unnecessary sudo's --- pms_api_rest/services/pms_calendar_service.py | 8 +- pms_api_rest/services/pms_folio_service.py | 38 ++++----- pms_api_rest/services/pms_partner_service.py | 8 +- .../services/pms_pricelist_service.py | 82 ++++++++----------- pms_api_rest/services/pms_property_service.py | 20 ++--- .../services/pms_reservation_service.py | 8 +- pms_api_rest/services/pms_room_service.py | 8 +- .../services/pms_room_type_services.py | 8 +- 8 files changed, 65 insertions(+), 115 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 47be040fa0..18da08f350 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -34,12 +34,8 @@ def get_calendar(self, calendar_search_param): ) result_lines = [] PmsCalendarInfo = self.env.datamodels["pms.calendar.info"] - for line in ( - self.env["pms.reservation.line"] - .sudo() - .search( - domain, - ) + for line in self.env["pms.reservation.line"].search( + domain, ): result_lines.append( PmsCalendarInfo( diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 8b1054b9a6..8d016b7096 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -31,16 +31,12 @@ def get_folios(self, folio_search_param): result_folios = [] reservations_result = ( - self.env["pms.reservation"].sudo().search(domain).mapped("folio_id").ids + self.env["pms.reservation"].search(domain).mapped("folio_id").ids ) PmsFolioInfo = self.env.datamodels["pms.folio.info"] - for folio in ( - self.env["pms.folio"] - .sudo() - .search( - [("id", "in", reservations_result)], - ) + for folio in self.env["pms.folio"].search( + [("id", "in", reservations_result)], ): reservations = [] for reservation in folio.reservation_ids: @@ -127,7 +123,7 @@ def get_folios(self, folio_search_param): auth="jwt_api_pms", ) def get_folio_payments(self, folio_id): - folio = self.env["pms.folio"].sudo().search([("id", "=", folio_id)]) + folio = self.env["pms.folio"].search([("id", "=", folio_id)]) payments = [] PmsPaymentInfo = self.env.datamodels["pms.payment.info"] if not folio: @@ -175,20 +171,16 @@ def get_folio_payments(self, folio_id): auth="jwt_api_pms", ) def create_reservation(self, pms_reservation_info): - reservation = ( - self.env["pms.reservation"] - .sudo() - .create( - { - "partner_name": pms_reservation_info.partner, - "pms_property_id": pms_reservation_info.property, - "room_type_id": pms_reservation_info.roomTypeId, - "pricelist_id": pms_reservation_info.pricelistId, - "checkin": pms_reservation_info.checkin, - "checkout": pms_reservation_info.checkout, - "board_service_room_id": pms_reservation_info.boardServiceId, - "channel_type_id": pms_reservation_info.channelTypeId, - } - ) + reservation = self.env["pms.reservation"].create( + { + "partner_name": pms_reservation_info.partner, + "pms_property_id": pms_reservation_info.property, + "room_type_id": pms_reservation_info.roomTypeId, + "pricelist_id": pms_reservation_info.pricelistId, + "checkin": pms_reservation_info.checkin, + "checkout": pms_reservation_info.checkout, + "board_service_room_id": pms_reservation_info.boardServiceId, + "channel_type_id": pms_reservation_info.channelTypeId, + } ) return reservation.id diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index ec48cd19ee..38f5eba88e 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -25,12 +25,8 @@ def get_partners(self): domain = [] result_partners = [] PmsPartnerInfo = self.env.datamodels["pms.partner.info"] - for partner in ( - self.env["res.partner"] - .sudo() - .search( - domain, - ) + for partner in self.env["res.partner"].search( + domain, ): result_partners.append( diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index b0e4785b43..fa044f284d 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -36,7 +36,7 @@ def get_pricelists(self, pricelist_info_search_param, **args): ) PmsPricelistInfo = self.env.datamodels["pms.pricelist.info"] result_pricelists = [] - for pricelist in self.env["product.pricelist"].sudo().search(domain): + for pricelist in self.env["product.pricelist"].search(domain): result_pricelists.append( PmsPricelistInfo( id=pricelist.id, @@ -60,66 +60,52 @@ def get_pricelists(self, pricelist_info_search_param, **args): ) def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): result = [] - record_pricelist_id = ( - self.env["product.pricelist"].sudo().search([("id", "=", pricelist_id)]) + record_pricelist_id = self.env["product.pricelist"].search( + [("id", "=", pricelist_id)] ) if not record_pricelist_id: raise MissingError PmsPricelistItemInfo = self.env.datamodels["pms.pricelist.item.info"] - rooms = ( - self.env["pms.room"] - .sudo() - .search( - [("pms_property_id", "=", pricelist_item_search_param.pms_property_id)] - ) + rooms = self.env["pms.room"].search( + [("pms_property_id", "=", pricelist_item_search_param.pms_property_id)] ) - for room_type in ( - self.env["pms.room.type"] - .sudo() - .search([("id", "in", rooms.mapped("room_type_id").ids)]) + for room_type in self.env["pms.room.type"].search( + [("id", "in", rooms.mapped("room_type_id").ids)] ): - for item in ( - self.env["product.pricelist.item"] - .sudo() - .search( + for item in self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", pricelist_id), + ("applied_on", "=", "0_product_variant"), + ("product_id", "=", room_type.product_id.id), + ( + "date_start_consumption", + ">=", + pricelist_item_search_param.date_from, + ), + ( + "date_end_consumption", + "<=", + pricelist_item_search_param.date_to, + ), + ] + ): + rule = self.env["pms.availability.plan.rule"].search( [ - ("pricelist_id", "=", pricelist_id), - ("applied_on", "=", "0_product_variant"), - ("product_id", "=", room_type.product_id.id), ( - "date_start_consumption", - ">=", - pricelist_item_search_param.date_from, + "availability_plan_id", + "=", + record_pricelist_id.availability_plan_id.id, ), + ("date", "=", item.date_start_consumption), + ("date", "=", item.date_end_consumption), + ("room_type_id", "=", room_type.id), ( - "date_end_consumption", - "<=", - pricelist_item_search_param.date_to, + "pms_property_id", + "=", + pricelist_item_search_param.pms_property_id, ), ] ) - ): - rule = ( - self.env["pms.availability.plan.rule"] - .sudo() - .search( - [ - ( - "availability_plan_id", - "=", - record_pricelist_id.availability_plan_id.id, - ), - ("date", "=", item.date_start_consumption), - ("date", "=", item.date_end_consumption), - ("room_type_id", "=", room_type.id), - ( - "pms_property_id", - "=", - pricelist_item_search_param.pms_property_id, - ), - ] - ) - ) rule.ensure_one() result.append( PmsPricelistItemInfo( diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index c5ce496b97..d3b8a211d2 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -30,12 +30,8 @@ def get_properties(self, property_search_param): domain.append(("id", "=", property_search_param.id)) result_properties = [] PmsPropertyInfo = self.env.datamodels["pms.property.info"] - for prop in ( - self.env["pms.property"] - .sudo() - .search( - domain, - ) + for prop in self.env["pms.property"].search( + domain, ): result_properties.append( PmsPropertyInfo( @@ -59,9 +55,7 @@ def get_properties(self, property_search_param): auth="jwt_api_pms", ) def get_property(self, property_id): - pms_property = ( - self.env["pms.property"].sudo().search([("id", "=", property_id)]) - ) + pms_property = self.env["pms.property"].search([("id", "=", property_id)]) res = [] PmsPropertyInfo = self.env.datamodels["pms.property.info"] if not pms_property: @@ -89,17 +83,15 @@ def get_property(self, property_id): ) def get_method_payments_property(self, property_id): - pms_property = ( - self.env["pms.property"].sudo().search([("id", "=", property_id)]) - ) + pms_property = self.env["pms.property"].search([("id", "=", property_id)]) PmsAccountJournalInfo = self.env.datamodels["pms.account.journal.info"] res = [] if not pms_property: pass else: for method in pms_property._get_payment_methods(automatic_included=True): - payment_method = ( - self.env["account.journal"].sudo().search([("id", "=", method.id)]) + payment_method = self.env["account.journal"].search( + [("id", "=", method.id)] ) res.append( PmsAccountJournalInfo( diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 972e8129a7..947347ef69 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -24,9 +24,7 @@ class PmsReservationService(Component): auth="jwt_api_pms", ) def get_reservation(self, reservation_id): - reservation = ( - self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) - ) + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) res = [] PmsReservationInfo = self.env.datamodels["pms.reservation.info"] if not reservation: @@ -181,9 +179,7 @@ def update_reservation(self, reservation_id, reservation_lines_changes): auth="jwt_api_pms", ) def get_checkin_partners(self, reservation_id): - reservation = ( - self.env["pms.reservation"].sudo().search([("id", "=", reservation_id)]) - ) + reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) checkin_partners = [] PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] if not reservation: diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 22985d823c..cfa4b1ec8a 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -33,12 +33,8 @@ def get_rooms(self, room_search_param): result_rooms = [] PmsRoomInfo = self.env.datamodels["pms.room.info"] - for room in ( - self.env["pms.room"] - .sudo() - .search( - domain, - ) + for room in self.env["pms.room"].search( + domain, ): result_rooms.append( diff --git a/pms_api_rest/services/pms_room_type_services.py b/pms_api_rest/services/pms_room_type_services.py index 45bd33c409..a15645f705 100644 --- a/pms_api_rest/services/pms_room_type_services.py +++ b/pms_api_rest/services/pms_room_type_services.py @@ -30,12 +30,8 @@ def get_room_types(self, room_type_search_param): domain.append(("id", "=", room_type_search_param.id)) result_rooms = [] PmsRoomTypeInfo = self.env.datamodels["pms.room.type.info"] - for room in ( - self.env["pms.room.type"] - .sudo() - .search( - domain, - ) + for room in self.env["pms.room.type"].search( + domain, ): result_rooms.append( From ba664bd66800f8720370d29668300318f897043e Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 31 Jan 2022 17:46:12 +0100 Subject: [PATCH 050/547] [IMP] pms_api_rest: add one property in query params --- pms_api_rest/datamodels/__init__.py | 2 ++ pms_api_rest/datamodels/pms_calendar.py | 2 ++ pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/datamodels/pms_reservation.py | 2 +- pms_api_rest/datamodels/pms_room.py | 2 +- pms_api_rest/datamodels/pms_search_param.py | 9 +++++++++ pms_api_rest/services/pms_calendar_service.py | 11 +++++----- pms_api_rest/services/pms_folio_service.py | 11 +++++++--- pms_api_rest/services/pms_property_service.py | 7 +------ .../services/pms_reservation_service.py | 20 +++++++++++++------ 10 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_search_param.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 01b14bdff1..9befd935e2 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -18,3 +18,5 @@ from . import pms_pricelist from . import pms_pricelist_item + +from . import pms_search_param diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index cffc2569a1..878a11ca6b 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -14,12 +14,14 @@ class PmsCalendarSwapInfo(Datamodel): swapTo = fields.String(required=True, allow_none=False) roomIdA = fields.Integer(required=True, allow_none=False) roomIdB = fields.Integer(required=True, allow_none=False) + pms_property_id = fields.Integer(required=True, allow_none=False) class PmsCalendarSearchParam(Datamodel): _name = "pms.calendar.search.param" date_from = fields.String(required=False, allow_none=True) date_to = fields.String(required=False, allow_none=True) + pms_property_id = fields.Integer(required=True, allow_none=False) class PmsCalendarInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index da9febb5ba..ffdad75a36 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -6,6 +6,7 @@ class PmsFolioSearchParam(Datamodel): _name = "pms.folio.search.param" + pms_property_id = fields.Integer(required=True, allow_none=True) date_from = fields.String(required=False, allow_none=True) date_to = 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 index 08f7bfffbc..f002fdf160 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -21,7 +21,7 @@ class PmsReservationInfo(Datamodel): pricelistId = fields.Integer(required=False, allow_none=True) services = fields.List(fields.Dict(required=False, allow_none=True)) messages = fields.List(fields.Dict(required=False, allow_none=True)) - property = fields.Integer(required=False, allow_none=True) + pms_property_id = fields.Integer(required=False, allow_none=True) boardServiceId = fields.Integer(required=False, allow_none=True) boardServiceName = fields.String(required=False, allow_none=True) channelTypeId = 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 index 61825ea491..ae9ed3acec 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -7,7 +7,7 @@ class PmsRoomSearchParam(Datamodel): _name = "pms.room.search.param" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - pms_property_id = fields.Integer(required=False, allow_none=True) + pms_property_id = fields.Integer(required=True, allow_none=False) class PmsRoomInfo(Datamodel): 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..03bdd07b5e --- /dev/null +++ b/pms_api_rest/datamodels/pms_search_param.py @@ -0,0 +1,9 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsSearchParam(Datamodel): + _name = "pms.search.param" + + pms_property_id = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 18da08f350..4acaa84ff4 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -26,12 +26,9 @@ class PmsCalendarService(Component): ) def get_calendar(self, calendar_search_param): domain = list() - domain.append( - ("date", ">", datetime.fromisoformat(calendar_search_param.date_from)) - ) - domain.append( - ("date", "<=", datetime.fromisoformat(calendar_search_param.date_to)) - ) + domain.append(("date", ">=", calendar_search_param.date_from)) + domain.append(("date", "<=", calendar_search_param.date_to)) + domain.append(("pms_property_id", "=", calendar_search_param.pms_property_id)) result_lines = [] PmsCalendarInfo = self.env.datamodels["pms.calendar.info"] for line in self.env["pms.reservation.line"].search( @@ -83,6 +80,7 @@ def swap_reservation_slices(self, swap_info): ("room_id", "=", room_id_a), ("date", ">=", swap_info.swapFrom), ("date", "<=", swap_info.swapTo), + ("pms_property_id", "=", swap_info.pms_property_id), ] ) @@ -91,6 +89,7 @@ def swap_reservation_slices(self, swap_info): ("room_id", "=", room_id_b), ("date", ">=", swap_info.swapFrom), ("date", "<=", swap_info.swapTo), + ("pms_property_id", "=", swap_info.pms_property_id), ] ) lines_room_a.occupies_availability = False diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 8d016b7096..4b95842df9 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -28,6 +28,7 @@ def get_folios(self, folio_search_param): domain = list() domain.append(("checkin", ">=", folio_search_param.date_from)) domain.append(("checkout", "<", folio_search_param.date_to)) + domain.append(("pms_property_id", "=", folio_search_param.pms_property_id)) result_folios = [] reservations_result = ( @@ -119,11 +120,15 @@ def get_folios(self, folio_search_param): "GET", ) ], + input_param=Datamodel("pms.search.param"), output_param=Datamodel("pms.payment.info", is_list=True), auth="jwt_api_pms", ) - def get_folio_payments(self, folio_id): - folio = self.env["pms.folio"].search([("id", "=", folio_id)]) + def get_folio_payments(self, folio_id, pms_search_param): + domain = list() + domain.append(("id", "=", folio_id)) + domain.append(("pms_property_id", "=", pms_search_param.pms_property_id)) + folio = self.env["pms.folio"].search(domain) payments = [] PmsPaymentInfo = self.env.datamodels["pms.payment.info"] if not folio: @@ -174,7 +179,7 @@ def create_reservation(self, pms_reservation_info): reservation = self.env["pms.reservation"].create( { "partner_name": pms_reservation_info.partner, - "pms_property_id": pms_reservation_info.property, + "pms_property_id": pms_reservation_info.pms_property_id, "room_type_id": pms_reservation_info.roomTypeId, "pricelist_id": pms_reservation_info.pricelistId, "checkin": pms_reservation_info.checkin, diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index d3b8a211d2..0c8a36d8bf 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -18,16 +18,11 @@ class PmsPropertyService(Component): "GET", ) ], - input_param=Datamodel("pms.property.search.param"), output_param=Datamodel("pms.property.info", is_list=True), auth="jwt_api_pms", ) - def get_properties(self, property_search_param): + def get_properties(self): domain = [] - if property_search_param.name: - domain.append(("name", "like", property_search_param.name)) - if property_search_param.id: - domain.append(("id", "=", property_search_param.id)) result_properties = [] PmsPropertyInfo = self.env.datamodels["pms.property.info"] for prop in self.env["pms.property"].search( diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 947347ef69..9dcc51df9e 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -20,11 +20,15 @@ class PmsReservationService(Component): "GET", ) ], - output_param=Datamodel("pms.reservation.info"), + 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): - reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) + def get_reservation(self, reservation_id, pms_search_param): + domain = list() + domain.append(("id", "=", reservation_id)) + domain.append(("pms_property_id", "=", pms_search_param.pms_property_id)) + reservation = self.env["pms.reservation"].search(domain) res = [] PmsReservationInfo = self.env.datamodels["pms.reservation.info"] if not reservation: @@ -59,7 +63,7 @@ def get_reservation(self, reservation_id): ) res = PmsReservationInfo( id=reservation.id, - partner=reservation.partner_id.name, + partner=reservation.partner_id.name if reservation.partner_id else "", checkin=str(reservation.checkin), checkout=str(reservation.checkout), preferredRoomId=reservation.preferred_room_id.id @@ -175,11 +179,15 @@ def update_reservation(self, reservation_id, reservation_lines_changes): "GET", ) ], + input_param=Datamodel("pms.search.param"), 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"].search([("id", "=", reservation_id)]) + def get_checkin_partners(self, reservation_id, pms_search_param): + domain = list() + domain.append(("id", "=", reservation_id)) + domain.append(("pms_property_id", "=", pms_search_param.pms_property_id)) + reservation = self.env["pms.reservation"].search(domain) checkin_partners = [] PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] if not reservation: From a8d09375d917f5da6663914657111af7ff1723fe Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 2 Feb 2022 16:43:48 +0100 Subject: [PATCH 051/547] [IMP] pms_api_rest: add several properties in query params --- pms_api_rest/datamodels/pms_pricelist.py | 2 +- pms_api_rest/datamodels/pms_room.py | 2 +- pms_api_rest/datamodels/pms_room_type.py | 2 ++ pms_api_rest/datamodels/pms_search_param.py | 3 +- .../services/pms_pricelist_service.py | 32 +++++++++++++------ .../services/pms_room_type_services.py | 27 ++++++++++++---- 6 files changed, 49 insertions(+), 19 deletions(-) diff --git a/pms_api_rest/datamodels/pms_pricelist.py b/pms_api_rest/datamodels/pms_pricelist.py index 6f2fd1d433..0b67c86b56 100644 --- a/pms_api_rest/datamodels/pms_pricelist.py +++ b/pms_api_rest/datamodels/pms_pricelist.py @@ -7,4 +7,4 @@ class PmsPricelistInfo(Datamodel): _name = "pms.pricelist.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - pms_property_id = fields.Integer(required=False, allow_none=True) + pms_property_ids = fields.List(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 index ae9ed3acec..61825ea491 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -7,7 +7,7 @@ class PmsRoomSearchParam(Datamodel): _name = "pms.room.search.param" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - pms_property_id = fields.Integer(required=True, allow_none=False) + pms_property_id = fields.Integer(required=False, allow_none=True) class PmsRoomInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_room_type.py b/pms_api_rest/datamodels/pms_room_type.py index c1bd369dc5..f0f65e2162 100644 --- a/pms_api_rest/datamodels/pms_room_type.py +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -7,9 +7,11 @@ class PmsRoomTypeSearchParam(Datamodel): _name = "pms.room.type.search.param" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) + pms_property_ids = 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) + pms_property_ids = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_search_param.py b/pms_api_rest/datamodels/pms_search_param.py index 03bdd07b5e..f71a4fe2dc 100644 --- a/pms_api_rest/datamodels/pms_search_param.py +++ b/pms_api_rest/datamodels/pms_search_param.py @@ -6,4 +6,5 @@ class PmsSearchParam(Datamodel): _name = "pms.search.param" - pms_property_id = fields.Integer(required=True, allow_none=False) + pms_property_id = fields.Integer(required=False, allow_none=True) + pms_property_ids = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index fa044f284d..6dfb243f9e 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -20,20 +20,31 @@ class PmsPricelistService(Component): "GET", ) ], - input_param=Datamodel("pms.pricelist.info", is_list=False), + input_param=Datamodel("pms.search.param", is_list=False), output_param=Datamodel("pms.pricelist.info", is_list=True), auth="jwt_api_pms", ) - def get_pricelists(self, pricelist_info_search_param, **args): - domain = [] - if pricelist_info_search_param.pms_property_id: - domain.append( - ( - "pms_property_ids", - "in", - [pricelist_info_search_param.pms_property_id], - ) + def get_pricelists(self, pms_search_param, **args): + + pricelists_all_properties = self.env["product.pricelist"].search( + [("pms_property_ids", "=", False)] + ) + pricelists = set() + for index, prop in enumerate(pms_search_param.pms_property_ids): + pricelists_with_query_property = self.env["product.pricelist"].search( + [("pms_property_ids", "=", prop)] ) + if index == 0: + pricelists = set(pricelists_with_query_property.ids) + else: + pricelists = pricelists.intersection( + set(pricelists_with_query_property.ids) + ) + pricelists_total = list(set(list(pricelists) + pricelists_all_properties.ids)) + domain = [ + ("id", "in", pricelists_total), + ] + PmsPricelistInfo = self.env.datamodels["pms.pricelist.info"] result_pricelists = [] for pricelist in self.env["product.pricelist"].search(domain): @@ -41,6 +52,7 @@ def get_pricelists(self, pricelist_info_search_param, **args): PmsPricelistInfo( id=pricelist.id, name=pricelist.name, + pms_property_ids=pricelist.pms_property_ids.mapped("id"), ) ) return result_pricelists diff --git a/pms_api_rest/services/pms_room_type_services.py b/pms_api_rest/services/pms_room_type_services.py index a15645f705..ade44d8d77 100644 --- a/pms_api_rest/services/pms_room_type_services.py +++ b/pms_api_rest/services/pms_room_type_services.py @@ -18,16 +18,30 @@ class PmsRoomTypeService(Component): "GET", ) ], - input_param=Datamodel("pms.room.search.param"), + input_param=Datamodel("pms.room.type.search.param"), output_param=Datamodel("pms.room.info", is_list=True), auth="jwt_api_pms", ) def get_room_types(self, room_type_search_param): - domain = [] - if room_type_search_param.name: - domain.append(("name", "like", room_type_search_param.name)) - if room_type_search_param.id: - domain.append(("id", "=", room_type_search_param.id)) + room_type_all_properties = self.env["pms.room.type"].search( + [("pms_property_ids", "=", False)] + ) + room_types = set() + for index, prop in enumerate(room_type_search_param.pms_property_ids): + 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)) + 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( @@ -38,6 +52,7 @@ def get_room_types(self, room_type_search_param): PmsRoomTypeInfo( id=room.id, name=room.name, + pms_property_ids=room.pms_property_ids.mapped("id"), ) ) return result_rooms From eeeaffe2ddadb8868c4255969adaaeaebbdbe8c1 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 3 Feb 2022 17:28:13 +0100 Subject: [PATCH 052/547] [IMP] pms_api_rest: show records with all properties when it isn't specified --- pms_api_rest/datamodels/pms_calendar.py | 2 +- .../services/pms_pricelist_service.py | 27 +++++++++++-------- .../services/pms_room_type_services.py | 27 +++++++++++-------- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 878a11ca6b..05d4cb7b46 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -14,7 +14,7 @@ class PmsCalendarSwapInfo(Datamodel): swapTo = fields.String(required=True, allow_none=False) roomIdA = fields.Integer(required=True, allow_none=False) roomIdB = fields.Integer(required=True, allow_none=False) - pms_property_id = fields.Integer(required=True, allow_none=False) + pms_property_id = fields.Integer(required=False, allow_none=True) class PmsCalendarSearchParam(Datamodel): diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 6dfb243f9e..179ef5a70d 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -29,18 +29,23 @@ def get_pricelists(self, pms_search_param, **args): pricelists_all_properties = self.env["product.pricelist"].search( [("pms_property_ids", "=", False)] ) - pricelists = set() - for index, prop in enumerate(pms_search_param.pms_property_ids): - pricelists_with_query_property = self.env["product.pricelist"].search( - [("pms_property_ids", "=", prop)] - ) - if index == 0: - pricelists = set(pricelists_with_query_property.ids) - else: - pricelists = pricelists.intersection( - set(pricelists_with_query_property.ids) + if pms_search_param.pms_property_ids: + pricelists = set() + for index, prop in enumerate(pms_search_param.pms_property_ids): + pricelists_with_query_property = self.env["product.pricelist"].search( + [("pms_property_ids", "=", prop)] ) - pricelists_total = list(set(list(pricelists) + pricelists_all_properties.ids)) + if index == 0: + pricelists = set(pricelists_with_query_property.ids) + else: + pricelists = pricelists.intersection( + set(pricelists_with_query_property.ids) + ) + pricelists_total = list( + set(list(pricelists) + pricelists_all_properties.ids) + ) + else: + pricelists_total = list(pricelists_all_properties.ids) domain = [ ("id", "in", pricelists_total), ] diff --git a/pms_api_rest/services/pms_room_type_services.py b/pms_api_rest/services/pms_room_type_services.py index ade44d8d77..544aea0a2d 100644 --- a/pms_api_rest/services/pms_room_type_services.py +++ b/pms_api_rest/services/pms_room_type_services.py @@ -26,18 +26,23 @@ def get_room_types(self, room_type_search_param): room_type_all_properties = self.env["pms.room.type"].search( [("pms_property_ids", "=", False)] ) - room_types = set() - for index, prop in enumerate(room_type_search_param.pms_property_ids): - 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) + if room_type_search_param.pms_property_ids: + room_types = set() + for index, prop in enumerate(room_type_search_param.pms_property_ids): + room_types_with_query_property = self.env["pms.room.type"].search( + [("pms_property_ids", "=", prop)] ) - room_types_total = list(set(list(room_types) + room_type_all_properties.ids)) + 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), ] From aa5e7d5bc0eec73d3363c835b087ca1d64c27c0f Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 4 Feb 2022 11:26:32 +0100 Subject: [PATCH 053/547] [IMP] pms_api_rest: show reservation when property isn't specified in url --- pms_api_rest/services/pms_reservation_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 9dcc51df9e..c583ba8acf 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -27,7 +27,8 @@ class PmsReservationService(Component): def get_reservation(self, reservation_id, pms_search_param): domain = list() domain.append(("id", "=", reservation_id)) - domain.append(("pms_property_id", "=", pms_search_param.pms_property_id)) + if pms_search_param.pms_property_id: + domain.append(("pms_property_id", "=", pms_search_param.pms_property_id)) reservation = self.env["pms.reservation"].search(domain) res = [] PmsReservationInfo = self.env.datamodels["pms.reservation.info"] From 2f7873404214c5c136d3300038d27775dbbba584 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 7 Feb 2022 16:44:38 +0100 Subject: [PATCH 054/547] [IMP] pms_api_rest: add patch reseration --- pms_api_rest/datamodels/pms_calendar.py | 7 +- .../services/pms_reservation_service.py | 112 +++++++++++------- 2 files changed, 73 insertions(+), 46 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 05d4cb7b46..062a9e0baf 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -3,9 +3,12 @@ from odoo.addons.datamodel.core import Datamodel -class PmsCalendarChanges(Datamodel): - _name = "pms.calendar.changes" +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) class PmsCalendarSwapInfo(Datamodel): diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index c583ba8acf..4e7c11757e 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -116,60 +116,84 @@ def get_reservation(self, reservation_id, pms_search_param): "PATCH", ) ], - input_param=Datamodel("pms.calendar.changes", is_list=False), + 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: - # 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"]), + # get date of first reservation id to change + first_reservation_line_id_to_change = ( + reservation_lines_changes.reservationLinesChanges[0][ + "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"] + 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"), ): - line_to_change.room_id = change_iterator["roomId"] + # 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" + 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" + ) ) - ) + 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 = self.env["pms.reservation"].browse(reservation_id) - reservation.checkin = min_value - reservation.checkout = max_value + reservation_vals = {} + + if reservation_lines_changes.preferredRoomId: + reservation_vals.update( + {"preferred_room_id": reservation_lines_changes.preferredRoomId} + ) + if reservation_lines_changes.boardServiceId: + 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} + ) + + reservation_to_update.write(reservation_vals) @restapi.method( [ From 5206558935dc3f96d32abf9dcca5d0bec164402b Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 15 Feb 2022 15:16:30 +0000 Subject: [PATCH 055/547] [IMP] pms: add user image base64 format as login response --- pms_api_rest/datamodels/pms_user.py | 7 +++++-- pms_api_rest/services/pms_login_service.py | 10 +++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index 83b03cef3f..80683707ef 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -11,6 +11,9 @@ class PmsApiRestUserInput(Datamodel): class PmsApiRestUserOutput(Datamodel): _name = "pms.api.rest.user.output" - # user = fields.String(required=False, allow_none=True) - # exp = fields.String(required=False, allow_none=True) token = fields.String(required=False, allow_none=True) + userId = fields.Integer(required=True, allow_none=False) + userName = fields.String(required=True, allow_none=False) + userImageBase64 = fields.String(required=False, allow_none=True) + defaultPropertyId = fields.Integer(required=True, allow_none=False) + defaultPropertyName = fields.String(required=True, allow_none=False) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index a719cc1879..281c5ab53e 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -52,4 +52,12 @@ def login(self, user): key="pms_secret_key_example", algorithm=jwt.ALGORITHMS.HS256, ) - return PmsApiRestUserOutput(token=token) + + return PmsApiRestUserOutput( + token=token, + userId=user_record.id, + userName=user_record.name, + defaultPropertyId=user_record.pms_property_id.id, + defaultPropertyName=user_record.pms_property_id.name, + userImageBase64=user_record.partner_id.image_1024, + ) From 85468a2193a3d4841d326fceb0fc8bc757ef1f02 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 18 Feb 2022 09:06:23 +0000 Subject: [PATCH 056/547] [IMP] pms: sort rooms by capacity @ room service --- pms_api_rest/services/pms_room_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index cfa4b1ec8a..641da6798b 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -35,7 +35,7 @@ def get_rooms(self, room_search_param): PmsRoomInfo = self.env.datamodels["pms.room.info"] for room in self.env["pms.room"].search( domain, - ): + ).sorted("capacity"): result_rooms.append( PmsRoomInfo( From 11eee72cdd412a5c762a6eaf4f5acbc5f13fbed4 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 18 Feb 2022 09:07:02 +0000 Subject: [PATCH 057/547] [IMP] pms: add fields to planning (reserv. line) --- pms_api_rest/datamodels/pms_calendar.py | 9 +++-- pms_api_rest/services/pms_calendar_service.py | 33 +++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 062a9e0baf..f9bea70522 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -41,9 +41,14 @@ class PmsCalendarInfo(Datamodel): reservationId = fields.Integer(required=False, allow_none=True) reservationName = fields.String(required=False, allow_none=True) reservationType = fields.String(required=False, allow_none=True) - isFirstDay = fields.Boolean(required=False, allow_none=True) - isLastDay = fields.Boolean(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) + hasNextLine = fields.Boolean(required=False, allow_none=True) + nextLineSplitted = fields.Boolean(required=False, allow_none=True) + previousLineSplitted = fields.Boolean(required=False, allow_none=True) + closureReason=fields.String(required=False, allow_none=True) + diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 4acaa84ff4..44b28cb9c2 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -34,6 +34,30 @@ def get_calendar(self, calendar_search_param): for line in self.env["pms.reservation.line"].search( domain, ): + next_line_splitted = False + next_line = self.env['pms.reservation.line'].search( + [ + ("reservation_id", "=", line.reservation_id.id), + ("date", "=", line.date + timedelta(days=1)) + ] + ) + if next_line: + next_line_splitted = next_line.room_id != line.room_id + + previous_line_splitted = False + previous_line = self.env['pms.reservation.line'].search( + [ + ("reservation_id", "=", line.reservation_id.id), + ("date", "=", line.date + timedelta(days=-1)) + ] + ) + if previous_line: + previous_line_splitted = previous_line.room_id != line.room_id + + closureReason= '' + if line.reservation_id.reservation_type == 'out': + print(line.reservation_id.closure_reason_id) + result_lines.append( PmsCalendarInfo( id=line.id, @@ -48,13 +72,16 @@ def get_calendar(self, calendar_search_param): reservationId=line.reservation_id, reservationName=line.reservation_id.name, reservationType=line.reservation_id.reservation_type, - isFirstDay=line.reservation_id.checkin == line.date, - isLastDay=line.reservation_id.checkout - == (line.date + timedelta(days=1)), + isFirstNight=line.reservation_id.checkin == line.date, + isLastNight=line.reservation_id.checkout + timedelta(days=-1) == line.date, totalPrice=line.reservation_id.price_total, pendingPayment=line.reservation_id.folio_pending_amount, numNotifications=len(line.reservation_id.message_ids), adults=line.reservation_id.adults, + nextLineSplitted=next_line_splitted, + previousLineSplitted=previous_line_splitted, + hasNextLine=bool(next_line), + closureReason='', ) ) return result_lines From 4a49eae8cf4bd2782bf37327576a2ba7a43e6f9d Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 24 Feb 2022 13:04:54 +0100 Subject: [PATCH 058/547] [IMP] pms_api_rest: add agency image in folio service and it's reservations --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index ffdad75a36..6398c5c5ef 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -26,3 +26,4 @@ class PmsFolioInfo(Datamodel): salesPerson = fields.String(required=False, allow_none=True) paymentState = fields.String(required=False, allow_none=True) propertyId = fields.Integer(required=False, allow_none=True) + agencyImage = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 4b95842df9..b259538496 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -80,6 +80,15 @@ def get_folios(self, folio_search_param): "folioId": reservation.folio_id.id if reservation.folio_id else "", + "saleChannel": reservation.channel_type_id.name + if reservation.channel_type_id + else "", + "agency": reservation.agency_id.name + if reservation.agency_id + else "", + "agencyImage": reservation.agency_id.image_1024.decode("utf-8") + if reservation.agency_id + else "", } ) result_folios.append( @@ -107,6 +116,7 @@ def get_folios(self, folio_search_param): if folio.payment_state else "", propertyId=folio.pms_property_id, + agencyImage=folio.agency_id.image_1024 if folio.agency_id else "", ) ) return result_folios From 0b64d2cd91d900cc2b342dbbac3270a7f1dafadb Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 1 Mar 2022 10:37:10 +0100 Subject: [PATCH 059/547] [IMP] pms_api_rest: add daily invoincing service --- pms_api_rest/services/pms_calendar_service.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 44b28cb9c2..60960b2ecf 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -128,3 +128,47 @@ def swap_reservation_slices(self, swap_info): lines_room_a._compute_occupies_availability() lines_room_b._compute_occupies_availability() + + @restapi.method( + [ + ( + [ + "/daily-invoicing", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.search.param", is_list=False), + auth="jwt_api_pms", + ) + def get_daily_invoincing(self, pms_calendar_search_param): + reservation_lines = self.env["pms.reservation.line"].search( + [ + ("date", ">=", pms_calendar_search_param.date_from), + ("date", "<=", pms_calendar_search_param.date_to), + ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), + ] + ) + service_lines = self.env["pms.service.line"].search( + [ + ("date", ">=", pms_calendar_search_param.date_from), + ("date", "<=", pms_calendar_search_param.date_to), + ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), + ] + ) + + date_from = datetime.strptime(pms_calendar_search_param.date_from, '%Y-%m-%d').date() + date_to = datetime.strptime(pms_calendar_search_param.date_to, '%Y-%m-%d').date() + + result = [] + for day in (date_from + timedelta(d) for d in range((date_to-date_from).days+1)): + reservation_lines_by_day = reservation_lines.filtered(lambda d: d.date == day) + service_lines_by_day = service_lines.filtered(lambda d: d.date == day) + daily_invoicing = { + "date": str(day), + "invoicing_total": + sum(reservation_lines_by_day.mapped("price"))+sum(service_lines_by_day.mapped("price_day_total")) + } + result.append(daily_invoicing) + + return result From 579b6c71fe3f7bf72414cbb5338ea2040513eb8b Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 1 Mar 2022 11:09:37 +0100 Subject: [PATCH 060/547] pre-commit --- pms_api_rest/datamodels/pms_calendar.py | 3 +- pms_api_rest/services/pms_calendar_service.py | 37 +++++++++++-------- pms_api_rest/services/pms_room_service.py | 10 +++-- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index f9bea70522..c5f6d26d32 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -50,5 +50,4 @@ class PmsCalendarInfo(Datamodel): hasNextLine = fields.Boolean(required=False, allow_none=True) nextLineSplitted = fields.Boolean(required=False, allow_none=True) previousLineSplitted = fields.Boolean(required=False, allow_none=True) - closureReason=fields.String(required=False, allow_none=True) - + closureReason = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 60960b2ecf..cd9f71a822 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -35,29 +35,25 @@ def get_calendar(self, calendar_search_param): domain, ): next_line_splitted = False - next_line = self.env['pms.reservation.line'].search( + next_line = self.env["pms.reservation.line"].search( [ ("reservation_id", "=", line.reservation_id.id), - ("date", "=", line.date + timedelta(days=1)) + ("date", "=", line.date + timedelta(days=1)), ] ) if next_line: next_line_splitted = next_line.room_id != line.room_id previous_line_splitted = False - previous_line = self.env['pms.reservation.line'].search( + previous_line = self.env["pms.reservation.line"].search( [ ("reservation_id", "=", line.reservation_id.id), - ("date", "=", line.date + timedelta(days=-1)) + ("date", "=", line.date + timedelta(days=-1)), ] ) if previous_line: previous_line_splitted = previous_line.room_id != line.room_id - closureReason= '' - if line.reservation_id.reservation_type == 'out': - print(line.reservation_id.closure_reason_id) - result_lines.append( PmsCalendarInfo( id=line.id, @@ -73,7 +69,8 @@ def get_calendar(self, calendar_search_param): reservationName=line.reservation_id.name, reservationType=line.reservation_id.reservation_type, isFirstNight=line.reservation_id.checkin == line.date, - isLastNight=line.reservation_id.checkout + timedelta(days=-1) == line.date, + isLastNight=line.reservation_id.checkout + timedelta(days=-1) + == line.date, totalPrice=line.reservation_id.price_total, pendingPayment=line.reservation_id.folio_pending_amount, numNotifications=len(line.reservation_id.message_ids), @@ -81,7 +78,7 @@ def get_calendar(self, calendar_search_param): nextLineSplitted=next_line_splitted, previousLineSplitted=previous_line_splitted, hasNextLine=bool(next_line), - closureReason='', + closureReason="", ) ) return result_lines @@ -157,17 +154,25 @@ def get_daily_invoincing(self, pms_calendar_search_param): ] ) - date_from = datetime.strptime(pms_calendar_search_param.date_from, '%Y-%m-%d').date() - date_to = datetime.strptime(pms_calendar_search_param.date_to, '%Y-%m-%d').date() + date_from = datetime.strptime( + pms_calendar_search_param.date_from, "%Y-%m-%d" + ).date() + date_to = datetime.strptime( + pms_calendar_search_param.date_to, "%Y-%m-%d" + ).date() result = [] - for day in (date_from + timedelta(d) for d in range((date_to-date_from).days+1)): - reservation_lines_by_day = reservation_lines.filtered(lambda d: d.date == day) + for day in ( + date_from + timedelta(d) for d in range((date_to - date_from).days + 1) + ): + reservation_lines_by_day = reservation_lines.filtered( + lambda d: d.date == day + ) service_lines_by_day = service_lines.filtered(lambda d: d.date == day) daily_invoicing = { "date": str(day), - "invoicing_total": - sum(reservation_lines_by_day.mapped("price"))+sum(service_lines_by_day.mapped("price_day_total")) + "invoicing_total": sum(reservation_lines_by_day.mapped("price")) + + sum(service_lines_by_day.mapped("price_day_total")), } result.append(daily_invoicing) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 641da6798b..d27c2a3d0d 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -33,9 +33,13 @@ def get_rooms(self, room_search_param): result_rooms = [] PmsRoomInfo = self.env.datamodels["pms.room.info"] - for room in self.env["pms.room"].search( - domain, - ).sorted("capacity"): + for room in ( + self.env["pms.room"] + .search( + domain, + ) + .sorted("capacity") + ): result_rooms.append( PmsRoomInfo( From a35013a7c3a7db3910462bd791ae15d7ee7b8459 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 1 Mar 2022 11:12:03 +0100 Subject: [PATCH 061/547] [IMP] pms_api_rest: change result of daily invoicing to camel case --- pms_api_rest/services/pms_calendar_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index cd9f71a822..8b42b0a5a9 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -171,7 +171,7 @@ def get_daily_invoincing(self, pms_calendar_search_param): service_lines_by_day = service_lines.filtered(lambda d: d.date == day) daily_invoicing = { "date": str(day), - "invoicing_total": sum(reservation_lines_by_day.mapped("price")) + "invoicingTotal": sum(reservation_lines_by_day.mapped("price")) + sum(service_lines_by_day.mapped("price_day_total")), } result.append(daily_invoicing) From 65c898caf5fcfea3c3297ac505f5c0e6623c2822 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 1 Mar 2022 11:38:37 +0100 Subject: [PATCH 062/547] [IMP] pms_api_rest: change date format in daily invoicing service result --- pms_api_rest/services/pms_calendar_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 8b42b0a5a9..7448171f7d 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -170,7 +170,7 @@ def get_daily_invoincing(self, pms_calendar_search_param): ) service_lines_by_day = service_lines.filtered(lambda d: d.date == day) daily_invoicing = { - "date": str(day), + "date": datetime.combine(day, datetime.min.time()).isoformat(), "invoicingTotal": sum(reservation_lines_by_day.mapped("price")) + sum(service_lines_by_day.mapped("price_day_total")), } From f46a1cb19daec33400b6242a43622e10608aa250 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 2 Mar 2022 17:38:40 +0100 Subject: [PATCH 063/547] [IMP] pms_api_rest: add free-rooms service --- pms_api_rest/datamodels/pms_calendar.py | 1 + pms_api_rest/services/pms_calendar_service.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index c5f6d26d32..b7221ecf02 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -25,6 +25,7 @@ class PmsCalendarSearchParam(Datamodel): date_from = fields.String(required=False, allow_none=True) date_to = fields.String(required=False, allow_none=True) pms_property_id = fields.Integer(required=True, allow_none=False) + pricelist_id = fields.Integer(required=False, allow_none=True) class PmsCalendarInfo(Datamodel): diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 7448171f7d..80781668c1 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -177,3 +177,61 @@ def get_daily_invoincing(self, pms_calendar_search_param): result.append(daily_invoicing) return result + + @restapi.method( + [ + ( + [ + "/free-rooms", + ], + "GET", + ) + ], + input_param=Datamodel("pms.calendar.search.param", is_list=False), + auth="jwt_api_pms", + ) + def get_free_rooms(self, pms_calendar_search_param): + + date_from = datetime.strptime( + pms_calendar_search_param.date_from, "%Y-%m-%d" + ).date() + date_to = datetime.strptime( + pms_calendar_search_param.date_to, "%Y-%m-%d" + ).date() + + self.env.cr.execute( + """ + SELECT date, + (SELECT COUNT(1) FROM pms_room WHERE pms_property_id = %s) - COUNT(1) free_rooms + FROM pms_reservation_line + WHERE occupies_availability = true + AND pms_property_id = %s + AND date >= %s + AND date <= %s + GROUP BY date + ORDER BY date; + """, + ( + pms_calendar_search_param.pms_property_id, + pms_calendar_search_param.pms_property_id, + date_from, + date_to, + ), + ) + reservation_lines = self.env.cr.fetchall() + + all_property_rooms = self.env["pms.room"].search( + [ + ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), + ] + ) + result = [] + for date, free_rooms in reservation_lines: + daily_free_rooms = { + "date": datetime.combine(date, datetime.min.time()).isoformat(), + "freeRooms": free_rooms, + "totalRooms": len(all_property_rooms), + } + result.append(daily_free_rooms) + + return result From def868247d6228144f2b0baf44f21110fedb060c Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 3 Mar 2022 11:27:55 +0100 Subject: [PATCH 064/547] [IMP] pms_api_rest: delete totalRooms in free-rooms service --- pms_api_rest/services/pms_calendar_service.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 80781668c1..fb888a5aff 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -220,17 +220,11 @@ def get_free_rooms(self, pms_calendar_search_param): ) reservation_lines = self.env.cr.fetchall() - all_property_rooms = self.env["pms.room"].search( - [ - ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), - ] - ) result = [] for date, free_rooms in reservation_lines: daily_free_rooms = { "date": datetime.combine(date, datetime.min.time()).isoformat(), "freeRooms": free_rooms, - "totalRooms": len(all_property_rooms), } result.append(daily_free_rooms) From 54b9fbf06b5aec7116741f1b1fb722b821c71aa5 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 4 Mar 2022 17:01:27 +0000 Subject: [PATCH 065/547] [IMP] pms_api_rest: change free daily rooms service (free rooms by type) & use datamodels (daily free rooms & daily invoicing) --- pms_api_rest/datamodels/pms_calendar.py | 13 +++ pms_api_rest/services/pms_calendar_service.py | 98 ++++++++++++------- 2 files changed, 75 insertions(+), 36 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index b7221ecf02..2b18db9f41 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -28,6 +28,19 @@ class PmsCalendarSearchParam(Datamodel): pricelist_id = fields.Integer(required=False, allow_none=True) +class PmsCalendarFreeDailyRoomsByType(Datamodel): + _name = "pms.calendar.free.daily.rooms.by.type" + date = fields.String(required=True, allow_none=False) + roomType = 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 PmsCalendarInfo(Datamodel): _name = "pms.calendar.info" id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index fb888a5aff..e31d329211 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -78,7 +78,9 @@ def get_calendar(self, calendar_search_param): nextLineSplitted=next_line_splitted, previousLineSplitted=previous_line_splitted, hasNextLine=bool(next_line), - closureReason="", + closureReason=line.reservation_id.closure_reason_id.name + if line.reservation_id.closure_reason_id + else "", ) ) return result_lines @@ -136,6 +138,7 @@ def swap_reservation_slices(self, swap_info): ) ], 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): @@ -162,6 +165,7 @@ def get_daily_invoincing(self, pms_calendar_search_param): ).date() result = [] + PmsCalendarDailyInvoicing = self.env.datamodels["pms.calendar.daily.invoicing"] for day in ( date_from + timedelta(d) for d in range((date_to - date_from).days + 1) ): @@ -169,12 +173,13 @@ def get_daily_invoincing(self, pms_calendar_search_param): lambda d: d.date == day ) service_lines_by_day = service_lines.filtered(lambda d: d.date == day) - daily_invoicing = { - "date": datetime.combine(day, datetime.min.time()).isoformat(), - "invoicingTotal": sum(reservation_lines_by_day.mapped("price")) - + sum(service_lines_by_day.mapped("price_day_total")), - } - result.append(daily_invoicing) + result.append( + PmsCalendarDailyInvoicing( + date=datetime.combine(day, datetime.min.time()).isoformat(), + invoicingTotal=sum(reservation_lines_by_day.mapped("price")) + + sum(service_lines_by_day.mapped("price_day_total")), + ) + ) return result @@ -188,6 +193,7 @@ def get_daily_invoincing(self, pms_calendar_search_param): ) ], 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): @@ -198,34 +204,54 @@ def get_free_rooms(self, pms_calendar_search_param): date_to = datetime.strptime( pms_calendar_search_param.date_to, "%Y-%m-%d" ).date() - - self.env.cr.execute( - """ - SELECT date, - (SELECT COUNT(1) FROM pms_room WHERE pms_property_id = %s) - COUNT(1) free_rooms - FROM pms_reservation_line - WHERE occupies_availability = true - AND pms_property_id = %s - AND date >= %s - AND date <= %s - GROUP BY date - ORDER BY date; - """, - ( - pms_calendar_search_param.pms_property_id, - pms_calendar_search_param.pms_property_id, - date_from, - date_to, - ), - ) - reservation_lines = self.env.cr.fetchall() - result = [] - for date, free_rooms in reservation_lines: - daily_free_rooms = { - "date": datetime.combine(date, datetime.min.time()).isoformat(), - "freeRooms": free_rooms, - } - result.append(daily_free_rooms) - + PmsCalendarFreeDailyRoomsByType = self.env.datamodels[ + "pms.calendar.free.daily.rooms.by.type" + ] + for date in ( + date_from + timedelta(d) for d in range((date_to - date_from).days + 1) + ): + rooms = self.env["pms.room"].search( + [("pms_property_id", "=", pms_calendar_search_param.pms_property_id)] + ) + for room_type_iterator in self.env["pms.room.type"].search( + [("id", "in", rooms.mapped("room_type_id").ids)] + ): + reservation_lines_room_type = self.env["pms.reservation.line"].search( + [ + ("date", "=", date), + ("occupies_availability", "=", True), + ("room_id.room_type_id", "=", room_type_iterator.id), + ( + "pms_property_id", + "=", + pms_calendar_search_param.pms_property_id, + ), + ] + ) + num_rooms_room_type = self.env["pms.room"].search_count( + [ + ( + "pms_property_id", + "=", + pms_calendar_search_param.pms_property_id, + ), + ("room_type_id", "=", room_type_iterator.id), + ] + ) + if not reservation_lines_room_type: + free_rooms_room_type = num_rooms_room_type + else: + free_rooms_room_type = num_rooms_room_type - len( + reservation_lines_room_type + ) + result.append( + PmsCalendarFreeDailyRoomsByType( + date=str( + datetime.combine(date, datetime.min.time()).isoformat() + ), + roomType=room_type_iterator.id, + freeRooms=free_rooms_room_type, + ) + ) return result From 724ac1dff03ddff1228bac44fefa2ab1a07569ab Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 7 Mar 2022 13:18:10 +0100 Subject: [PATCH 066/547] [IMP] pms_api_rest: change pricelist_items service adding restrictions --- .../services/pms_pricelist_service.py | 95 +++++++++++-------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 179ef5a70d..e4e1de4a77 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -1,3 +1,5 @@ +from datetime import datetime, timedelta + from odoo.exceptions import MissingError from odoo.addons.base_rest import restapi @@ -86,35 +88,45 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): rooms = self.env["pms.room"].search( [("pms_property_id", "=", pricelist_item_search_param.pms_property_id)] ) - for room_type in self.env["pms.room.type"].search( - [("id", "in", rooms.mapped("room_type_id").ids)] + date_from = datetime.strptime( + pricelist_item_search_param.date_from, "%Y-%m-%d" + ).date() + date_to = datetime.strptime( + pricelist_item_search_param.date_to, "%Y-%m-%d" + ).date() + + for date in ( + date_from + timedelta(d) for d in range((date_to - date_from).days + 1) ): - for item in self.env["product.pricelist.item"].search( - [ - ("pricelist_id", "=", pricelist_id), - ("applied_on", "=", "0_product_variant"), - ("product_id", "=", room_type.product_id.id), - ( - "date_start_consumption", - ">=", - pricelist_item_search_param.date_from, - ), - ( - "date_end_consumption", - "<=", - pricelist_item_search_param.date_to, - ), - ] + for room_type in self.env["pms.room.type"].search( + [("id", "in", rooms.mapped("room_type_id").ids)] ): + item = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", pricelist_id), + ("applied_on", "=", "0_product_variant"), + ("product_id", "=", room_type.product_id.id), + ( + "date_start_consumption", + ">=", + date, + ), + ( + "date_end_consumption", + "<=", + date, + ), + ] + ) + rule = self.env["pms.availability.plan.rule"].search( [ + ("date", "=", date), ( "availability_plan_id", "=", record_pricelist_id.availability_plan_id.id, ), - ("date", "=", item.date_start_consumption), - ("date", "=", item.date_end_consumption), ("room_type_id", "=", room_type.id), ( "pms_property_id", @@ -123,23 +135,32 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): ), ] ) - rule.ensure_one() - result.append( - PmsPricelistItemInfo( - pricelist_item_id=item.id, - availability_rule_id=rule.id, + + if item or rule: + pricelist_info = PmsPricelistItemInfo( room_type_id=room_type.id, - fixed_price=item.fixed_price, - min_stay=rule.min_stay, - min_stay_arrival=rule.min_stay_arrival, - max_stay=rule.max_stay, - max_stay_arrival=rule.max_stay_arrival, - closed=rule.closed, - closed_departure=rule.closed_departure, - closed_arrival=rule.closed_arrival, - quota=rule.quota, - max_avail=rule.max_avail, - date=str(item.date_start_consumption), + date=str( + datetime.combine(date, datetime.min.time()).isoformat() + ), ) - ) + + if item: + pricelist_info.pricelist_item_id = item.id + pricelist_info.fixed_price = item.fixed_price + + if rule: + + pricelist_info.availability_rule_id = rule.id + pricelist_info.min_stay = 8 + pricelist_info.min_stay_arrival = rule.min_stay_arrival + pricelist_info.max_stay = rule.max_stay + pricelist_info.max_stay_arrival = rule.max_stay_arrival + pricelist_info.closed = rule.closed + pricelist_info.closed_departure = rule.closed_departure + pricelist_info.closed_arrival = rule.closed_arrival + pricelist_info.quota = rule.quota + pricelist_info.max_avail = rule.max_avail + + result.append(pricelist_info) + return result From 1f4ca397b943c9ff70d149387b901fdaa0101c88 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 11 Apr 2022 15:42:45 +0100 Subject: [PATCH 067/547] [IMP] pms_api_rest: jwt exp. date --- pms_api_rest/datamodels/pms_calendar.py | 2 +- pms_api_rest/datamodels/pms_user.py | 1 + pms_api_rest/services/pms_calendar_service.py | 2 +- pms_api_rest/services/pms_login_service.py | 9 +++++++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 2b18db9f41..cd690b60ed 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -31,7 +31,7 @@ class PmsCalendarSearchParam(Datamodel): class PmsCalendarFreeDailyRoomsByType(Datamodel): _name = "pms.calendar.free.daily.rooms.by.type" date = fields.String(required=True, allow_none=False) - roomType = fields.Integer(required=True, allow_none=False) + roomTypeId = fields.Integer(required=True, allow_none=False) freeRooms = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index 80683707ef..a3b3900710 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -12,6 +12,7 @@ class PmsApiRestUserInput(Datamodel): class PmsApiRestUserOutput(Datamodel): _name = "pms.api.rest.user.output" token = fields.String(required=False, allow_none=True) + expirationDate = fields.Integer(required=True, allow_none=False) userId = fields.Integer(required=True, allow_none=False) userName = fields.String(required=True, allow_none=False) userImageBase64 = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index e31d329211..4b9eec9754 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -250,7 +250,7 @@ def get_free_rooms(self, pms_calendar_search_param): date=str( datetime.combine(date, datetime.min.time()).isoformat() ), - roomType=room_type_iterator.id, + roomTypeId=room_type_iterator.id, freeRooms=free_rooms_room_type, ) ) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 281c5ab53e..81bfd6558b 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -1,4 +1,5 @@ import time +from math import ceil from jose import jwt @@ -35,17 +36,20 @@ def login(self, user): user_record = ( self.env["res.users"].sudo().search([("login", "=", user.username)]) ) + # formula = ms_now + ms in 1 sec * secs in 1 min + minutes = 10 + timestamp_expire_in_a_min = int(time.time()*1000.0) + 1000 * 60 * minutes if not user_record: raise ValidationError(_("user or password not valid")) user_record.with_user(user_record)._check_credentials(user.password, None) PmsApiRestUserOutput = self.env.datamodels["pms.api.rest.user.output"] - expiration_date = time.time() + 36660 + token = jwt.encode( { "aud": "api_pms", "iss": "pms", - "exp": expiration_date, + "exp": timestamp_expire_in_a_min, "username": user.username, "password": user.password, }, @@ -55,6 +59,7 @@ def login(self, user): return PmsApiRestUserOutput( token=token, + expirationDate=timestamp_expire_in_a_min, userId=user_record.id, userName=user_record.name, defaultPropertyId=user_record.pms_property_id.id, From 698da32f6d5450a8ea67e075a7ad6280bd94e5af Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 13 Apr 2022 14:11:47 +0100 Subject: [PATCH 068/547] [FIX] pms-api-rest: fix service reservation lines (prev. loading cancel res. lines) --- pms_api_rest/services/pms_calendar_service.py | 1 + pms_api_rest/services/pms_login_service.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 4b9eec9754..dd6e0b56ce 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -29,6 +29,7 @@ def get_calendar(self, calendar_search_param): domain.append(("date", ">=", calendar_search_param.date_from)) domain.append(("date", "<=", calendar_search_param.date_to)) domain.append(("pms_property_id", "=", calendar_search_param.pms_property_id)) + domain.append(("state", "!=", "cancel")) result_lines = [] PmsCalendarInfo = self.env.datamodels["pms.calendar.info"] for line in self.env["pms.reservation.line"].search( diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 81bfd6558b..3a3aa3839c 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -1,5 +1,4 @@ import time -from math import ceil from jose import jwt @@ -38,7 +37,7 @@ def login(self, user): ) # formula = ms_now + ms in 1 sec * secs in 1 min minutes = 10 - timestamp_expire_in_a_min = int(time.time()*1000.0) + 1000 * 60 * minutes + timestamp_expire_in_a_min = int(time.time() * 1000.0) + 1000 * 60 * minutes if not user_record: raise ValidationError(_("user or password not valid")) From bb6b40cd1df8f63f30781b34e54effbcc468f8ee Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 13 Apr 2022 15:44:53 +0200 Subject: [PATCH 069/547] [IMP] pms_api_rest: add default_pricelist in property and fix result in pricelist_service --- pms_api_rest/datamodels/pms_property.py | 1 + pms_api_rest/services/pms_pricelist_service.py | 2 +- pms_api_rest/services/pms_property_service.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index 6a10fa855b..24664c7d7c 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -15,3 +15,4 @@ class PmsPropertyInfo(Datamodel): id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) company = fields.String(required=False, allow_none=True) + defaultPricelistId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index e4e1de4a77..5835e68ddf 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -151,7 +151,7 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): if rule: pricelist_info.availability_rule_id = rule.id - pricelist_info.min_stay = 8 + pricelist_info.min_stay = rule.min_stay pricelist_info.min_stay_arrival = rule.min_stay_arrival pricelist_info.max_stay = rule.max_stay pricelist_info.max_stay_arrival = rule.max_stay_arrival diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 0c8a36d8bf..59cb8865c3 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -33,6 +33,7 @@ def get_properties(self): id=prop.id, name=prop.name, company=prop.company_id.name, + defaultPricelistId=prop.default_pricelist_id.id, ) ) return result_properties @@ -60,6 +61,7 @@ def get_property(self, property_id): id=pms_property.id, name=pms_property.name, company=pms_property.company_id.name, + defaultPricelistId=pms_property.default_pricelist_id.id, ) return res From 5b96501a153757a16db2072daa0779aaf053d691 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 6 May 2022 17:41:52 +0200 Subject: [PATCH 070/547] [IMP]pms_api_rest: added short_name in room datamodel and service and added default_code in room_type datamodel and service --- pms_api_rest/datamodels/pms_room.py | 1 + pms_api_rest/datamodels/pms_room_type.py | 1 + pms_api_rest/services/pms_room_service.py | 1 + pms_api_rest/services/pms_room_type_services.py | 1 + 4 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index 61825ea491..15f3bc3786 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -16,3 +16,4 @@ class PmsRoomInfo(Datamodel): 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) diff --git a/pms_api_rest/datamodels/pms_room_type.py b/pms_api_rest/datamodels/pms_room_type.py index f0f65e2162..46112cf52f 100644 --- a/pms_api_rest/datamodels/pms_room_type.py +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -15,3 +15,4 @@ class PmsRoomTypeInfo(Datamodel): id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) pms_property_ids = fields.List(fields.Integer(), required=False) + defaultCode = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index d27c2a3d0d..364bab5c13 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -47,6 +47,7 @@ def get_rooms(self, room_search_param): name=room.name, roomTypeId=room.room_type_id, capacity=room.capacity, + shortName=room.short_name, ) ) return result_rooms diff --git a/pms_api_rest/services/pms_room_type_services.py b/pms_api_rest/services/pms_room_type_services.py index 544aea0a2d..6a2c9085b3 100644 --- a/pms_api_rest/services/pms_room_type_services.py +++ b/pms_api_rest/services/pms_room_type_services.py @@ -58,6 +58,7 @@ def get_room_types(self, room_type_search_param): id=room.id, name=room.name, pms_property_ids=room.pms_property_ids.mapped("id"), + defaultCode=room.default_code, ) ) return result_rooms From 60c193a70a389ab2fc27c2f7e5d29c7a5a526857 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 21 Apr 2022 18:40:54 +0200 Subject: [PATCH 071/547] [IMP]pms: added configuration and color code for reservation segments in the property --- pms/models/pms_property.py | 79 ++++++++++++++++++++++++++++++++ pms/views/pms_property_views.xml | 67 +++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index 63ff0ee50c..cb699f7119 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -243,6 +243,85 @@ class PmsProperty(models.Model): default=False, ) + 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)", + ) + @api.depends_context( "checkin", "checkout", diff --git a/pms/views/pms_property_views.xml b/pms/views/pms_property_views.xml index 415f260326..8da488f716 100644 --- a/pms/views/pms_property_views.xml +++ b/pms/views/pms_property_views.xml @@ -88,6 +88,73 @@ + + + + + + + + + + + + + + + + + + + From a9079840c27ab04a91d1b3f8aa54a2db3940d096 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 21 Apr 2022 18:42:45 +0200 Subject: [PATCH 072/547] [IMP]pms_api_rest: added color code for reservations in property datamodel and service --- pms_api_rest/datamodels/pms_property.py | 13 +++++++++++ pms_api_rest/services/pms_property_service.py | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index 24664c7d7c..c345d86175 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -16,3 +16,16 @@ class PmsPropertyInfo(Datamodel): name = 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) + 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) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 59cb8865c3..539bb560dd 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -34,6 +34,19 @@ def get_properties(self): name=prop.name, company=prop.company_id.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, + simpleOutColor=prop.simple_out_color, + simpleInColor=prop.simple_in_color, + simpleFutureColor=prop.simple_future_color, ) ) return result_properties @@ -62,6 +75,16 @@ def get_property(self, 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, ) return res From 30e2202f230f766a9937efda211e9bff45878a17 Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 25 Apr 2022 11:04:59 +0200 Subject: [PATCH 073/547] [REF] pms_api_rest: changed the number of messages in the service by the message_needaction_counter field --- pms_api_rest/services/pms_calendar_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index dd6e0b56ce..9c809ee656 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -74,7 +74,7 @@ def get_calendar(self, calendar_search_param): == line.date, totalPrice=line.reservation_id.price_total, pendingPayment=line.reservation_id.folio_pending_amount, - numNotifications=len(line.reservation_id.message_ids), + numNotifications=line.reservation_id.message_needaction_counter, adults=line.reservation_id.adults, nextLineSplitted=next_line_splitted, previousLineSplitted=previous_line_splitted, From 82be93646f99c8973771a5dfe2a19aa579c2e9ea Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 25 Apr 2022 18:04:42 +0200 Subject: [PATCH 074/547] [IMP] pms_api_rest: change field names in pricelist datamodel --- pms_api_rest/datamodels/pms_pricelist_item.py | 21 ++++++++----------- .../services/pms_pricelist_service.py | 21 ++++++++----------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/pms_api_rest/datamodels/pms_pricelist_item.py b/pms_api_rest/datamodels/pms_pricelist_item.py index 8ec3c285f0..351ed7ac5c 100644 --- a/pms_api_rest/datamodels/pms_pricelist_item.py +++ b/pms_api_rest/datamodels/pms_pricelist_item.py @@ -12,17 +12,14 @@ class PmsPricelistItemSearchParam(Datamodel): class PmsPricelistItemInfo(Datamodel): _name = "pms.pricelist.item.info" - pricelist_item_id = fields.Integer(required=False, allow_none=True) - availability_rule_id = fields.Integer(required=False, allow_none=True) - fixed_price = fields.Float(required=False, allow_none=True) - min_stay = fields.Integer(required=False, allow_none=True) - min_stay_arrival = fields.Integer(required=False, allow_none=True) - max_stay = fields.Integer(required=False, allow_none=True) - max_stay_arrival = fields.Integer(required=False, allow_none=True) + pricelistItemId = fields.Integer(required=False, allow_none=True) + 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) - closed_departure = fields.Boolean(required=False, allow_none=True) - closed_arrival = fields.Boolean(required=False, allow_none=True) - quota = fields.Integer(required=False, allow_none=True) - max_avail = fields.Integer(required=False, allow_none=True) - room_type_id = fields.Integer(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) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 5835e68ddf..19ad1a2b42 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -138,28 +138,25 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): if item or rule: pricelist_info = PmsPricelistItemInfo( - room_type_id=room_type.id, + roomTypeId=room_type.id, date=str( datetime.combine(date, datetime.min.time()).isoformat() ), ) if item: - pricelist_info.pricelist_item_id = item.id - pricelist_info.fixed_price = item.fixed_price + pricelist_info.pricelistItemId = item.id if rule: - pricelist_info.availability_rule_id = rule.id - pricelist_info.min_stay = rule.min_stay - pricelist_info.min_stay_arrival = rule.min_stay_arrival - pricelist_info.max_stay = rule.max_stay - pricelist_info.max_stay_arrival = rule.max_stay_arrival + pricelist_info.availabilityRuleId = rule.id + pricelist_info.minStay = rule.min_stay + pricelist_info.minStayArrival = rule.min_stay_arrival + pricelist_info.maxStay = rule.max_stay + pricelist_info.maxStayArrival = rule.max_stay_arrival pricelist_info.closed = rule.closed - pricelist_info.closed_departure = rule.closed_departure - pricelist_info.closed_arrival = rule.closed_arrival - pricelist_info.quota = rule.quota - pricelist_info.max_avail = rule.max_avail + pricelist_info.closedDeparture = rule.closed_departure + pricelist_info.closedArrival = rule.closed_arrival result.append(pricelist_info) From 1751fbbbe2b4a22a1cc32e315f95a3aa40e719d2 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 27 Apr 2022 11:38:36 +0200 Subject: [PATCH 075/547] [REF]pms_api_rest: changed property colors from module pms to module pms_api_rest --- pms/models/pms_property.py | 79 --------------------- pms/views/pms_property_views.xml | 67 ------------------ pms_api_rest/__init__.py | 1 + pms_api_rest/__manifest__.py | 5 +- pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/pms_property.py | 84 +++++++++++++++++++++++ pms_api_rest/views/pms_property_views.xml | 78 +++++++++++++++++++++ 7 files changed, 168 insertions(+), 147 deletions(-) create mode 100644 pms_api_rest/models/__init__.py create mode 100644 pms_api_rest/models/pms_property.py create mode 100644 pms_api_rest/views/pms_property_views.xml diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index cb699f7119..63ff0ee50c 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -243,85 +243,6 @@ class PmsProperty(models.Model): default=False, ) - 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)", - ) - @api.depends_context( "checkin", "checkout", diff --git a/pms/views/pms_property_views.xml b/pms/views/pms_property_views.xml index 8da488f716..415f260326 100644 --- a/pms/views/pms_property_views.xml +++ b/pms/views/pms_property_views.xml @@ -88,73 +88,6 @@ - - - - - - - - - - - - - - - - - - - diff --git a/pms_api_rest/__init__.py b/pms_api_rest/__init__.py index 5b9cd7bd00..4204207672 100644 --- a/pms_api_rest/__init__.py +++ b/pms_api_rest/__init__.py @@ -1,3 +1,4 @@ from . import controllers from . import datamodels from . import services +from . import models diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 7bf98aa078..f7af4d0ef7 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -16,6 +16,9 @@ "external_dependencies": { "python": ["jwt", "simplejson", "marshmallow", "jose"], }, - "data": ["data/auth_jwt_validator.xml"], + "data": [ + "data/auth_jwt_validator.xml", + "views/pms_property_views.xml", + ], "installable": True, } diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py new file mode 100644 index 0000000000..9216f6ffd4 --- /dev/null +++ b/pms_api_rest/models/__init__.py @@ -0,0 +1 @@ +from . import pms_property diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py new file mode 100644 index 0000000000..a9c224253f --- /dev/null +++ b/pms_api_rest/models/pms_property.py @@ -0,0 +1,84 @@ +from odoo import fields, models + + +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)", + ) 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..cc3afd76e2 --- /dev/null +++ b/pms_api_rest/views/pms_property_views.xml @@ -0,0 +1,78 @@ + + + + pms.property + + + + + + + + + + + + + + + + + + + + + + + + + + From 0ed7ea9578dace94304c8b844f3524ca3abad994 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 2 May 2022 15:13:04 +0100 Subject: [PATCH 076/547] [FIX] pms-api-rest: fix domain service properties (user permission) --- pms_api_rest/services/pms_property_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 539bb560dd..72a6ba297a 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -22,7 +22,7 @@ class PmsPropertyService(Component): auth="jwt_api_pms", ) def get_properties(self): - domain = [] + 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( From 14c126252e5385a3f5c03a34f69f2d33008f7da6 Mon Sep 17 00:00:00 2001 From: Sara Date: Mon, 2 May 2022 17:22:37 +0200 Subject: [PATCH 077/547] [IMP] pms_api_rest: add filters in get_folios --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 34 +++++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 6398c5c5ef..5c8360ee0c 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -9,6 +9,7 @@ class PmsFolioSearchParam(Datamodel): pms_property_id = fields.Integer(required=True, allow_none=True) date_from = fields.String(required=False, allow_none=True) date_to = fields.String(required=False, allow_none=True) + filter = fields.String(required=False, allow_none=True) class PmsFolioInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index b259538496..624b1822d0 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1,5 +1,7 @@ from datetime import datetime +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 @@ -25,10 +27,33 @@ class PmsFolioService(Component): auth="jwt_api_pms", ) def get_folios(self, folio_search_param): - domain = list() - domain.append(("checkin", ">=", folio_search_param.date_from)) - domain.append(("checkout", "<", folio_search_param.date_to)) - domain.append(("pms_property_id", "=", folio_search_param.pms_property_id)) + domain_fields = list() + + domain_fields.append( + ("pms_property_id", "=", folio_search_param.pms_property_id) + ) + + if folio_search_param.date_to and folio_search_param.date_from: + domain_fields.append(("checkin", ">=", folio_search_param.date_from)) + domain_fields.append(("checkout", "<", folio_search_param.date_to)) + + domain_filter = list() + if folio_search_param.filter: + for search in folio_search_param.filter.split(" "): + subdomains = [ + [("name", "ilike", search)], + [("folio_id.name", "ilike", search)], + [("partner_name", "ilike", search)], + [("partner_id.firstname", "ilike", search)], + [("partner_id.lastname", "ilike", search)], + [("partner_id.id_numbers.name", "ilike", search)], + ] + domain_filter.append(expression.OR(subdomains)) + domain = [] + if domain_filter: + domain = expression.AND([domain_fields, domain_filter[0]]) + else: + domain = domain_fields result_folios = [] reservations_result = ( @@ -54,6 +79,7 @@ def get_folios(self, folio_search_param): reservations.append( { "id": reservation.id, + "name": reservation.name, "checkin": datetime.combine( reservation.checkin, datetime.min.time() ).isoformat(), From abfd091e374e2295fb8e3c4ee4166106ac1a6e69 Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 25 Apr 2022 20:17:57 +0200 Subject: [PATCH 078/547] [IMP]pms_api_rest: added alerts per day service and datamodel --- pms_api_rest/datamodels/pms_calendar.py | 6 +++ pms_api_rest/services/pms_calendar_service.py | 42 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index cd690b60ed..c5166653c9 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -65,3 +65,9 @@ class PmsCalendarInfo(Datamodel): nextLineSplitted = fields.Boolean(required=False, allow_none=True) previousLineSplitted = fields.Boolean(required=False, allow_none=True) closureReason = fields.String(required=False, allow_none=True) + + +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/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 9c809ee656..eb546561bc 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -256,3 +256,45 @@ def get_free_rooms(self, pms_calendar_search_param): ) ) 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): + PmsCalendarAlertsPerDay = self.env.datamodels["pms.calendar.alerts.per.day"] + date_from = datetime.strptime( + pms_calendar_search_param.date_from, "%Y-%m-%d" + ).date() + date_to = datetime.strptime( + pms_calendar_search_param.date_to, "%Y-%m-%d" + ).date() + result = [] + for day in ( + date_from + timedelta(d) for d in range((date_to - date_from).days + 1) + ): + overbooking = False + reservations = self.env["pms.reservation"].search( + [ + ("checkin", "=", day), + ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), + ] + ) + if any(reservation.overbooking for reservation in reservations): + overbooking = True + result.append( + PmsCalendarAlertsPerDay( + date=str(datetime.combine(day, datetime.min.time()).isoformat()), + overbooking=overbooking, + ) + ) + return result From c503b2ff5d0a16f7e8d891a92aa170f4148dac94 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 29 Apr 2022 17:19:49 +0200 Subject: [PATCH 079/547] [IMP]pms_api_rest: changed reservation overbooking for reservation line overbooking in planning alert per days service --- pms_api_rest/services/pms_calendar_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index eb546561bc..8aa661fb71 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -283,13 +283,13 @@ def get_alerts_per_day(self, pms_calendar_search_param): date_from + timedelta(d) for d in range((date_to - date_from).days + 1) ): overbooking = False - reservations = self.env["pms.reservation"].search( + lines = self.env["pms.reservation.line"].search( [ - ("checkin", "=", day), + ("date", "=", day), ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), ] ) - if any(reservation.overbooking for reservation in reservations): + if any(line.overbooking for line in lines): overbooking = True result.append( PmsCalendarAlertsPerDay( From 226bf1488378a26ada580aa5ec8e2102f08afe80 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 4 May 2022 17:22:46 +0200 Subject: [PATCH 080/547] [REF]pms_api_rest: refactoring of get_alerts_by_day service --- pms_api_rest/services/pms_calendar_service.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 8aa661fb71..5b536a0228 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -282,19 +282,17 @@ def get_alerts_per_day(self, pms_calendar_search_param): for day in ( date_from + timedelta(d) for d in range((date_to - date_from).days + 1) ): - overbooking = False - lines = self.env["pms.reservation.line"].search( + lines = self.env["pms.reservation.line"].search_count( [ ("date", "=", day), ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), + ("overbooking", "=", True), ] ) - if any(line.overbooking for line in lines): - overbooking = True result.append( PmsCalendarAlertsPerDay( date=str(datetime.combine(day, datetime.min.time()).isoformat()), - overbooking=overbooking, + overbooking=True if lines > 0 else False, ) ) return result From c711029cd92bb3919aba2317aa9d2b1d2a7911c8 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 4 May 2022 13:12:30 +0200 Subject: [PATCH 081/547] [IMP]pms_api_rest: added folioId in calendar datamodel and service --- pms_api_rest/datamodels/pms_calendar.py | 1 + pms_api_rest/services/pms_calendar_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index c5166653c9..d539f2448b 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -52,6 +52,7 @@ class PmsCalendarInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 5b536a0228..5dcf796694 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -66,6 +66,7 @@ def get_calendar(self, calendar_search_param): splitted=line.reservation_id.splitted, partnerId=line.reservation_id.partner_id.id or None, partnerName=line.reservation_id.partner_name or None, + folioId=line.reservation_id.folio_id, reservationId=line.reservation_id, reservationName=line.reservation_id.name, reservationType=line.reservation_id.reservation_type, From 14aa08d4a9939373f1089cab4de5ad950dd44d2b Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 19 May 2022 15:07:26 +0100 Subject: [PATCH 082/547] [FIX] manage error responses from login --- pms_api_rest/__init__.py | 1 + pms_api_rest/http.py | 87 ++++++++++++++++++++++ pms_api_rest/services/pms_login_service.py | 11 ++- 3 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 pms_api_rest/http.py diff --git a/pms_api_rest/__init__.py b/pms_api_rest/__init__.py index 4204207672..e95aba4aba 100644 --- a/pms_api_rest/__init__.py +++ b/pms_api_rest/__init__.py @@ -2,3 +2,4 @@ from . import datamodels from . import services from . import models +from . import http diff --git a/pms_api_rest/http.py b/pms_api_rest/http.py new file mode 100644 index 0000000000..fae00d1470 --- /dev/null +++ b/pms_api_rest/http.py @@ -0,0 +1,87 @@ +from werkzeug.exceptions import InternalServerError, Unauthorized, NotFound, Forbidden, BadRequest, HTTPException + +import odoo +from odoo.addons.base_rest.http import HttpRestRequest, wrapJsonException +from odoo.addons.base_rest.http import _rest_services_routes +from odoo.exceptions import MissingError, AccessError, AccessDenied, UserError, ValidationError +from odoo.http import Root, SessionExpiredException, HttpRequest +from odoo.loglevels import ustr + + +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 + print('session expired 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: + print('access error / access denied exception') + extra_info = getattr(e, "rest_json_info", None) + return wrapJsonException( + Forbidden(ustr(e)), + include_description=True, + extra_info=extra_info + ) + except (UserError, ValidationError) 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: + print('Unauthorized exception') + 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/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 3a3aa3839c..673349cc01 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -1,9 +1,10 @@ import time +import werkzeug.exceptions from jose import jwt from odoo import _ -from odoo.exceptions import ValidationError +from odoo.exceptions import AccessDenied, UserError from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -40,8 +41,12 @@ def login(self, user): timestamp_expire_in_a_min = int(time.time() * 1000.0) + 1000 * 60 * minutes if not user_record: - raise ValidationError(_("user or password not valid")) - user_record.with_user(user_record)._check_credentials(user.password, None) + 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")) + PmsApiRestUserOutput = self.env.datamodels["pms.api.rest.user.output"] token = jwt.encode( From 06366cd06448df28608ac66a80167953c4fd3fd1 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 23 May 2022 12:09:41 +0100 Subject: [PATCH 083/547] [IMP] pms-api-rest: service room (ex for other services) --- pms_api_rest/services/pms_room_service.py | 101 ++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 364bab5c13..1bd5facd3e 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -1,6 +1,8 @@ +from odoo import _ 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 PmsRoomService(Component): @@ -51,3 +53,102 @@ def get_rooms(self, room_search_param): ) ) 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, + ) + else: + raise MissingError(_("Room not found")) + + @restapi.method( + [ + ( + [ + "/", + ], + "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) + ] + ) + if room: + room.name = pms_room_info_data.name + else: + raise MissingError(_("Room not found")) + + @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 From 241f850c5f9e6652afe872e9a7ec4001f018c2f1 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 23 May 2022 12:10:53 +0100 Subject: [PATCH 084/547] [FIX] pms-api-rest: fix precommit --- pms_api_rest/http.py | 69 ++++++++++++---------- pms_api_rest/services/pms_login_service.py | 2 +- pms_api_rest/services/pms_room_service.py | 25 +++----- 3 files changed, 45 insertions(+), 51 deletions(-) diff --git a/pms_api_rest/http.py b/pms_api_rest/http.py index fae00d1470..3e65ed52a7 100644 --- a/pms_api_rest/http.py +++ b/pms_api_rest/http.py @@ -1,12 +1,29 @@ -from werkzeug.exceptions import InternalServerError, Unauthorized, NotFound, Forbidden, BadRequest, HTTPException +from werkzeug.exceptions import ( + BadRequest, + Forbidden, + HTTPException, + InternalServerError, + NotFound, + Unauthorized, +) import odoo -from odoo.addons.base_rest.http import HttpRestRequest, wrapJsonException -from odoo.addons.base_rest.http import _rest_services_routes -from odoo.exceptions import MissingError, AccessError, AccessDenied, UserError, ValidationError -from odoo.http import Root, SessionExpiredException, HttpRequest +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): @@ -14,61 +31,49 @@ def __init__(self, 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.""" + 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 - print('session expired exception') + print("session expired 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 + NotFound(ustr(e)), include_description=True, extra_info=extra_info ) except (AccessError, AccessDenied) as e: - print('access error / access denied exception') + print("access error / access denied exception") extra_info = getattr(e, "rest_json_info", None) return wrapJsonException( - Forbidden(ustr(e)), - include_description=True, - extra_info=extra_info + Forbidden(ustr(e)), include_description=True, extra_info=extra_info ) except (UserError, ValidationError) as e: extra_info = getattr(e, "rest_json_info", None) return wrapJsonException( - BadRequest(e.args[0]), - include_description=True, - extra_info=extra_info + 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 - ) + return wrapJsonException(e, include_description=True, extra_info=extra_info) except Unauthorized as e: - print('Unauthorized exception') + print("Unauthorized exception") extra_info = getattr(e, "rest_json_info", None) - return wrapJsonException( - e, - include_description=True, - extra_info=extra_info), + 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 - ) + 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): diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 673349cc01..bd6f3148b9 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -4,7 +4,7 @@ from jose import jwt from odoo import _ -from odoo.exceptions import AccessDenied, UserError +from odoo.exceptions import AccessDenied from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 1bd5facd3e..00d4277986 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -1,8 +1,9 @@ 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 odoo.exceptions import MissingError class PmsRoomService(Component): @@ -67,11 +68,7 @@ def get_rooms(self, room_search_param): auth="jwt_api_pms", ) def get_room(self, room_id): - room = self.env['pms.room'].search( - [ - ('id', '=', room_id) - ] - ) + room = self.env["pms.room"].search([("id", "=", room_id)]) if room: PmsRoomInfo = self.env.datamodels["pms.room.info"] return PmsRoomInfo( @@ -97,11 +94,7 @@ def get_room(self, room_id): auth="jwt_api_pms", ) def update_room(self, room_id, pms_room_info_data): - room = self.env['pms.room'].search( - [ - ('id', '=', room_id) - ] - ) + room = self.env["pms.room"].search([("id", "=", room_id)]) if room: room.name = pms_room_info_data.name else: @@ -120,11 +113,7 @@ def update_room(self, room_id, pms_room_info_data): ) def delete_room(self, room_id): # esto tb podría ser con un browse - room = self.env['pms.room'].search( - [ - ('id', '=', room_id) - ] - ) + room = self.env["pms.room"].search([("id", "=", room_id)]) if room: room.active = False else: @@ -143,12 +132,12 @@ def delete_room(self, room_id): auth="jwt_api_pms", ) def create_room(self, pms_room_info_param): - room = self.env['pms.room'].create( + 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 + "short_name": pms_room_info_param.shortName, } ) return room.id From 2a3a5b7f908db870e58b4cd93306f35606dd3bb1 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 11 May 2022 17:14:46 +0200 Subject: [PATCH 085/547] [IMP]pms_api_rest: added price in room_type service and datamodel --- pms_api_rest/datamodels/pms_pricelist_item.py | 9 +---- pms_api_rest/datamodels/pms_room_type.py | 1 + .../services/pms_pricelist_service.py | 33 +++---------------- .../services/pms_room_type_services.py | 1 + 4 files changed, 7 insertions(+), 37 deletions(-) diff --git a/pms_api_rest/datamodels/pms_pricelist_item.py b/pms_api_rest/datamodels/pms_pricelist_item.py index 351ed7ac5c..83fb083571 100644 --- a/pms_api_rest/datamodels/pms_pricelist_item.py +++ b/pms_api_rest/datamodels/pms_pricelist_item.py @@ -13,13 +13,6 @@ class PmsPricelistItemSearchParam(Datamodel): class PmsPricelistItemInfo(Datamodel): _name = "pms.pricelist.item.info" pricelistItemId = fields.Integer(required=False, allow_none=True) - 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) + price = fields.Float(required=False, allow_none=True) roomTypeId = fields.Integer(required=False, allow_none=True) date = 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 index 46112cf52f..eb4045ad16 100644 --- a/pms_api_rest/datamodels/pms_room_type.py +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -16,3 +16,4 @@ class PmsRoomTypeInfo(Datamodel): name = fields.String(required=False, allow_none=True) pms_property_ids = fields.List(fields.Integer(), required=False) defaultCode = fields.String(required=False, allow_none=True) + price = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 19ad1a2b42..73b39f4924 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -1,3 +1,4 @@ +import re from datetime import datetime, timedelta from odoo.exceptions import MissingError @@ -119,24 +120,7 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): ] ) - rule = self.env["pms.availability.plan.rule"].search( - [ - ("date", "=", date), - ( - "availability_plan_id", - "=", - record_pricelist_id.availability_plan_id.id, - ), - ("room_type_id", "=", room_type.id), - ( - "pms_property_id", - "=", - pricelist_item_search_param.pms_property_id, - ), - ] - ) - - if item or rule: + if item: pricelist_info = PmsPricelistItemInfo( roomTypeId=room_type.id, date=str( @@ -146,17 +130,8 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): if item: pricelist_info.pricelistItemId = item.id - - if rule: - - pricelist_info.availabilityRuleId = rule.id - pricelist_info.minStay = rule.min_stay - pricelist_info.minStayArrival = rule.min_stay_arrival - pricelist_info.maxStay = rule.max_stay - pricelist_info.maxStayArrival = rule.max_stay_arrival - pricelist_info.closed = rule.closed - pricelist_info.closedDeparture = rule.closed_departure - pricelist_info.closedArrival = rule.closed_arrival + price = re.findall("[+-]?\d+\.\d+", item.price) + pricelist_info.price = float(price[0]) result.append(pricelist_info) diff --git a/pms_api_rest/services/pms_room_type_services.py b/pms_api_rest/services/pms_room_type_services.py index 6a2c9085b3..448d747891 100644 --- a/pms_api_rest/services/pms_room_type_services.py +++ b/pms_api_rest/services/pms_room_type_services.py @@ -59,6 +59,7 @@ def get_room_types(self, room_type_search_param): name=room.name, pms_property_ids=room.pms_property_ids.mapped("id"), defaultCode=room.default_code, + price=room.list_price, ) ) return result_rooms From 5c3d09363dc7a87c9cebc8a5338fe69498cb462b Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 11 May 2022 17:15:01 +0200 Subject: [PATCH 086/547] [IMP]pms_api_rest: removed availability plan rules from pricelist service and datamodel --- pms_api_rest/services/pms_pricelist_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 73b39f4924..e924963fb6 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -130,7 +130,7 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): if item: pricelist_info.pricelistItemId = item.id - price = re.findall("[+-]?\d+\.\d+", item.price) + price = re.findall(r"[+-]?\d+\.\d+", item.price) pricelist_info.price = float(price[0]) result.append(pricelist_info) From 357616c2adf83a7dbb9b71a1daa6b4b355e71c0c Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 11 May 2022 17:21:53 +0200 Subject: [PATCH 087/547] [IMP]pms_api_rest: added default availability plan in property service and datamodel --- pms_api_rest/datamodels/pms_property.py | 1 + pms_api_rest/services/pms_property_service.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index c345d86175..f5f556c231 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -16,6 +16,7 @@ class PmsPropertyInfo(Datamodel): name = fields.String(required=False, allow_none=True) company = fields.String(required=False, allow_none=True) defaultPricelistId = fields.Integer(required=False, allow_none=True) + defaultAvailabilityPlanId = 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) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 72a6ba297a..542b935c56 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -34,6 +34,7 @@ def get_properties(self): name=prop.name, company=prop.company_id.name, defaultPricelistId=prop.default_pricelist_id.id, + defaultAvailabilityPlanId=prop.default_pricelist_id.availability_plan_id.id, colorOptionConfig=prop.color_option_config, preReservationColor=prop.pre_reservation_color, confirmedReservationColor=prop.confirmed_reservation_color, @@ -65,6 +66,7 @@ def get_properties(self): ) def get_property(self, property_id): pms_property = self.env["pms.property"].search([("id", "=", property_id)]) + default_avail = pms_property.default_pricelist_id.availability_plan_id.id res = [] PmsPropertyInfo = self.env.datamodels["pms.property.info"] if not pms_property: @@ -75,6 +77,7 @@ def get_property(self, property_id): name=pms_property.name, company=pms_property.company_id.name, defaultPricelistId=pms_property.default_pricelist_id.id, + defaultAvailabilityPlanId=default_avail, colorOptionConfig=pms_property.color_option_config, preReservationColor=pms_property.pre_reservation_color, confirmedReservationColor=pms_property.confirmed_reservation_color, From 8cac98f8f0b7d41d6f75ba714485d1c1f5ce12c3 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 11 May 2022 17:23:35 +0200 Subject: [PATCH 088/547] [IMP]pms_api_rest: added availability plan service and datamodel --- pms_api_rest/datamodels/__init__.py | 3 +- .../datamodels/pms_availability_plan.py | 10 ++ .../datamodels/pms_availability_plan_rule.py | 25 +++ pms_api_rest/services/__init__.py | 1 + .../services/pms_availability_plan_service.py | 151 ++++++++++++++++++ 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 pms_api_rest/datamodels/pms_availability_plan.py create mode 100644 pms_api_rest/datamodels/pms_availability_plan_rule.py create mode 100644 pms_api_rest/services/pms_availability_plan_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 9befd935e2..ecb00454a4 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -18,5 +18,6 @@ from . import pms_pricelist from . import pms_pricelist_item - +from . import pms_availability_plan +from . import pms_availability_plan_rule from . import pms_search_param 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..9e5cd2fb07 --- /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) + pms_property_ids = 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..4496938588 --- /dev/null +++ b/pms_api_rest/datamodels/pms_availability_plan_rule.py @@ -0,0 +1,25 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAvailabilityPlanRuleSearchParam(Datamodel): + _name = "pms.availability.plan.rule.search.param" + date_from = fields.String(required=True, allow_none=False) + date_to = fields.String(required=True, allow_none=False) + pms_property_id = 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) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index f6249f90a5..2763228ea7 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -8,3 +8,4 @@ from . import pms_property_service from . import pms_login_service from . import pms_pricelist_service +from . import pms_availability_plan_service 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..20d867939e --- /dev/null +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -0,0 +1,151 @@ +from datetime import datetime, timedelta + +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 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.pms_property_ids: + for index, prop in enumerate(pms_search_param.pms_property_ids): + 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, + pms_property_ids=availability.pms_property_ids.mapped("id"), + ) + ) + return result_availabilities + + @restapi.method( + [ + ( + [ + "/", + ], + "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 + ): + result = [] + record_availability_plan_id = self.env["pms.availability.plan"].browse( + availability_plan_id + ) + if not record_availability_plan_id: + raise MissingError + PmsAvailabilityPlanInfo = self.env.datamodels["pms.availability.plan.rule.info"] + rooms = self.env["pms.room"].search( + [ + ( + "pms_property_id", + "=", + availability_plan_rule_search_param.pms_property_id, + ) + ] + ) + date_from = datetime.strptime( + availability_plan_rule_search_param.date_from, "%Y-%m-%d" + ).date() + date_to = datetime.strptime( + availability_plan_rule_search_param.date_to, "%Y-%m-%d" + ).date() + + for date in ( + date_from + timedelta(d) for d in range((date_to - date_from).days + 1) + ): + for room_type in self.env["pms.room.type"].search( + [("id", "in", rooms.mapped("room_type_id").ids)] + ): + rule = self.env["pms.availability.plan.rule"].search( + [ + ("date", "=", date), + ( + "availability_plan_id", + "=", + record_availability_plan_id.id, + ), + ("room_type_id", "=", room_type.id), + ] + ) + if rule: + availability_plan_rule_info = PmsAvailabilityPlanInfo( + roomTypeId=room_type.id, + date=str( + datetime.combine(date, datetime.min.time()).isoformat() + ), + ) + + if rule: + + availability_plan_rule_info.availabilityRuleId = rule.id + availability_plan_rule_info.minStay = rule.min_stay + availability_plan_rule_info.minStayArrival = ( + rule.min_stay_arrival + ) + availability_plan_rule_info.maxStay = rule.max_stay + availability_plan_rule_info.maxStayArrival = ( + rule.max_stay_arrival + ) + availability_plan_rule_info.closed = rule.closed + availability_plan_rule_info.closedDeparture = ( + rule.closed_departure + ) + availability_plan_rule_info.closedArrival = rule.closed_arrival + availability_plan_rule_info.quota = rule.quota + + result.append(availability_plan_rule_info) + + return result From 9bc7eb1a5164bde00ce802656aa069508272d035 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 20 May 2022 13:00:04 +0200 Subject: [PATCH 089/547] [IMP]pms_api_rest: added post/patch for pricelist_items and avail_plan_rules --- .../datamodels/pms_availability_plan_rule.py | 1 + pms_api_rest/datamodels/pms_pricelist_item.py | 1 + .../services/pms_availability_plan_service.py | 91 +++++++++++++++++++ .../services/pms_pricelist_service.py | 61 +++++++++++++ 4 files changed, 154 insertions(+) diff --git a/pms_api_rest/datamodels/pms_availability_plan_rule.py b/pms_api_rest/datamodels/pms_availability_plan_rule.py index 4496938588..6e600a4c34 100644 --- a/pms_api_rest/datamodels/pms_availability_plan_rule.py +++ b/pms_api_rest/datamodels/pms_availability_plan_rule.py @@ -23,3 +23,4 @@ class PmsAvailabilityPlanRuleInfo(Datamodel): roomTypeId = fields.Integer(required=False, allow_none=True) date = fields.String(required=False, allow_none=True) quota = fields.Integer(required=False, allow_none=True) + pmsPropertyId = 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 index 83fb083571..988ad691dd 100644 --- a/pms_api_rest/datamodels/pms_pricelist_item.py +++ b/pms_api_rest/datamodels/pms_pricelist_item.py @@ -16,3 +16,4 @@ class PmsPricelistItemInfo(Datamodel): price = fields.Float(required=False, allow_none=True) roomTypeId = fields.Integer(required=False, allow_none=True) date = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 20d867939e..6c95184ef1 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -149,3 +149,94 @@ def get_availability_plan_rules( result.append(availability_plan_rule_info) return result + + @restapi.method( + [ + ( + [ + "//availability-plan-rule", + ], + "POST", + ) + ], + input_param=Datamodel("pms.availability.plan.rule.info", is_list=False), + auth="jwt_api_pms", + ) + def create_availability_plan_rule( + self, availability_plan_id, pms_avail_plan_rule_info + ): + day = datetime.strptime( + pms_avail_plan_rule_info.date[:10], "%Y-%m-%d" + ) + timedelta(days=1) + vals = { + "room_type_id": pms_avail_plan_rule_info.roomTypeId, + "date": day, + "pms_property_id": pms_avail_plan_rule_info.pmsPropertyId, + "availability_plan_id": availability_plan_id, + } + + if pms_avail_plan_rule_info.minStay: + vals.update({"min_stay": pms_avail_plan_rule_info.minStay}) + if pms_avail_plan_rule_info.minStayArrival: + vals.update({"min_stay_arrival": pms_avail_plan_rule_info.minStayArrival}) + if pms_avail_plan_rule_info.maxStay: + vals.update({"max_stay": pms_avail_plan_rule_info.maxStay}) + if pms_avail_plan_rule_info.maxStayArrival: + vals.update({"max_stay_arrival": pms_avail_plan_rule_info.maxStayArrival}) + if pms_avail_plan_rule_info.closed: + vals.update({"closed": pms_avail_plan_rule_info.closed}) + if pms_avail_plan_rule_info.closedDeparture: + vals.update({"closed_departure": pms_avail_plan_rule_info.closedDeparture}) + if pms_avail_plan_rule_info.closedArrival: + vals.update({"closed_arrival": pms_avail_plan_rule_info.closedArrival}) + if pms_avail_plan_rule_info.quota: + vals.update({"quota": pms_avail_plan_rule_info.quota}) + avail_plan_rule = self.env["pms.availability.plan.rule"].create(vals) + return avail_plan_rule.id + + @restapi.method( + [ + ( + [ + "//availability-plan-rule/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.availability.plan.rule.info", is_list=False), + auth="jwt_api_pms", + ) + def write_availability_plan_rule( + self, availability_plan_id, pms_avail_plan_rule_info + ): + vals = dict() + avail_rule = self.env["pms.availability.plan.rule"].search( + [ + ("id", "=", pms_avail_plan_rule_info.availabilityRuleId), + ("availability_plan_id", "=", availability_plan_id), + ] + ) + if avail_rule: + if pms_avail_plan_rule_info.minStay: + vals.update({"min_stay": pms_avail_plan_rule_info.minStay}) + if pms_avail_plan_rule_info.minStayArrival: + vals.update( + {"min_stay_arrival": pms_avail_plan_rule_info.minStayArrival} + ) + if pms_avail_plan_rule_info.maxStay: + vals.update({"max_stay": pms_avail_plan_rule_info.maxStay}) + if pms_avail_plan_rule_info.maxStayArrival: + vals.update( + {"max_stay_arrival": pms_avail_plan_rule_info.maxStayArrival} + ) + if pms_avail_plan_rule_info.closed: + vals.update({"closed": pms_avail_plan_rule_info.closed}) + if pms_avail_plan_rule_info.closedDeparture: + vals.update( + {"closed_departure": pms_avail_plan_rule_info.closedDeparture} + ) + if pms_avail_plan_rule_info.closedArrival: + vals.update({"closed_arrival": pms_avail_plan_rule_info.closedArrival}) + if pms_avail_plan_rule_info.quota: + vals.update({"quota": pms_avail_plan_rule_info.quota}) + avail_rule.write(vals) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index e924963fb6..4c400adf96 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -136,3 +136,64 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): result.append(pricelist_info) return result + + @restapi.method( + [ + ( + [ + "//pricelist-item", + ], + "POST", + ) + ], + input_param=Datamodel("pms.pricelist.item.info", is_list=False), + auth="jwt_api_pms", + ) + def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): + day = datetime.strptime( + pms_pricelist_item_info.date[:10], "%Y-%m-%d" + ) + timedelta(days=1) + product_id = ( + self.env["pms.room.type"] + .browse(pms_pricelist_item_info.roomTypeId) + .product_id + ) + pricelist_item = self.env["product.pricelist.item"].create( + { + "applied_on": "0_product_variant", + "product_id": product_id.id, + "pms_property_ids": [pms_pricelist_item_info.pmsPropertyId], + "date_start_consumption": day, + "date_end_consumption": day, + "compute_price": "fixed", + "fixed_price": pms_pricelist_item_info.price, + "pricelist_id": pricelist_id, + } + ) + return pricelist_item.id + + @restapi.method( + [ + ( + [ + "//pricelist-item", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.pricelist.item.info", is_list=False), + auth="jwt_api_pms", + ) + def write_pricelist_item(self, pricelist_id, pms_pricelist_item_info): + product_pricelist_item = self.env["product.pricelist.item"].search( + [ + ("id", "=", pms_pricelist_item_info.pricelistItemId), + ("pricelist_id", "=", pricelist_id), + ] + ) + if product_pricelist_item and pms_pricelist_item_info.price: + product_pricelist_item.write( + { + "fixed_price": pms_pricelist_item_info.price, + } + ) From 4a39b96c61eeb3aa682ae2a79b4113078ece0c80 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 17 Jun 2022 15:41:34 +0200 Subject: [PATCH 090/547] [FIX] pms-api-rest: fix service av. plan rules --- pms_api_rest/services/pms_availability_plan_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 6c95184ef1..fcd17e5c2a 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -68,7 +68,7 @@ def get_availability_plans(self, pms_search_param, **args): [ ( [ - "/", + "//rules", ], "GET", ) From 3ea7aa2f5966cf621dd091421782c3fb7806acb9 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 17 Jun 2022 17:27:50 +0200 Subject: [PATCH 091/547] [FIX] pms-api-rest: fix redundand cond @ av. plan serv. --- .../services/pms_availability_plan_service.py | 32 ++++++------------- pms_api_rest/services/pms_login_service.py | 2 +- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index fcd17e5c2a..8ad0b56d45 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -123,29 +123,17 @@ def get_availability_plan_rules( if rule: availability_plan_rule_info = PmsAvailabilityPlanInfo( roomTypeId=room_type.id, - date=str( - datetime.combine(date, datetime.min.time()).isoformat() - ), + 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, ) - - if rule: - - availability_plan_rule_info.availabilityRuleId = rule.id - availability_plan_rule_info.minStay = rule.min_stay - availability_plan_rule_info.minStayArrival = ( - rule.min_stay_arrival - ) - availability_plan_rule_info.maxStay = rule.max_stay - availability_plan_rule_info.maxStayArrival = ( - rule.max_stay_arrival - ) - availability_plan_rule_info.closed = rule.closed - availability_plan_rule_info.closedDeparture = ( - rule.closed_departure - ) - availability_plan_rule_info.closedArrival = rule.closed_arrival - availability_plan_rule_info.quota = rule.quota - result.append(availability_plan_rule_info) return result diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index bd6f3148b9..72ea073acd 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -37,7 +37,7 @@ def login(self, user): self.env["res.users"].sudo().search([("login", "=", user.username)]) ) # formula = ms_now + ms in 1 sec * secs in 1 min - minutes = 10 + minutes = 10000 timestamp_expire_in_a_min = int(time.time() * 1000.0) + 1000 * 60 * minutes if not user_record: From bbe7f75b0ded501a142fa65203e39ce38272bc1e Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 22 Jun 2022 09:54:13 +0200 Subject: [PATCH 092/547] [REF] pms-api-rest: fix nested roues & add plurals --- .../services/pms_availability_plan_service.py | 11 ++++++----- pms_api_rest/services/pms_pricelist_service.py | 18 +++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 8ad0b56d45..e7483fe0d2 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -68,7 +68,7 @@ def get_availability_plans(self, pms_search_param, **args): [ ( [ - "//rules", + "//availability-plan-rules", ], "GET", ) @@ -142,7 +142,7 @@ def get_availability_plan_rules( [ ( [ - "//availability-plan-rule", + "//availability-plan-rules", ], "POST", ) @@ -186,7 +186,7 @@ def create_availability_plan_rule( [ ( [ - "//availability-plan-rule/", + "//availability-plan-rules/", ], "PATCH", ) @@ -195,13 +195,14 @@ def create_availability_plan_rule( auth="jwt_api_pms", ) def write_availability_plan_rule( - self, availability_plan_id, pms_avail_plan_rule_info + self, availability_plan_id, availability_plan_rule_id, pms_avail_plan_rule_info ): vals = dict() avail_rule = self.env["pms.availability.plan.rule"].search( [ - ("id", "=", pms_avail_plan_rule_info.availabilityRuleId), ("availability_plan_id", "=", availability_plan_id), + ("id", "=", availability_plan_rule_id), + ] ) if avail_rule: diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 4c400adf96..bbf76713cb 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -69,7 +69,7 @@ def get_pricelists(self, pms_search_param, **args): [ ( [ - "/", + "//pricelist-items", ], "GET", ) @@ -128,10 +128,9 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): ), ) - if item: - pricelist_info.pricelistItemId = item.id - price = re.findall(r"[+-]?\d+\.\d+", item.price) - pricelist_info.price = float(price[0]) + pricelist_info.pricelistItemId = item.id + price = re.findall(r"[+-]?\d+\.\d+", item.price) + pricelist_info.price = float(price[0]) result.append(pricelist_info) @@ -141,7 +140,7 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): [ ( [ - "//pricelist-item", + "//pricelist-items", ], "POST", ) @@ -176,7 +175,7 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): [ ( [ - "//pricelist-item", + "//pricelist-items/", ], "PATCH", ) @@ -184,11 +183,12 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): input_param=Datamodel("pms.pricelist.item.info", is_list=False), auth="jwt_api_pms", ) - def write_pricelist_item(self, pricelist_id, pms_pricelist_item_info): + def write_pricelist_item(self, pricelist_id, pricelist_item_id, pms_pricelist_item_info): + product_pricelist_item = self.env["product.pricelist.item"].search( [ - ("id", "=", pms_pricelist_item_info.pricelistItemId), ("pricelist_id", "=", pricelist_id), + ("id", "=", pricelist_item_id), ] ) if product_pricelist_item and pms_pricelist_item_info.price: From 1c8eb69fa19d174229b16d45a28f1f59040f238a Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 13 Jun 2022 20:38:23 +0200 Subject: [PATCH 093/547] [IMP]pms_api_rest: added reservation fields in get_folios service --- pms_api_rest/services/pms_folio_service.py | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 624b1822d0..ba887e5354 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -73,13 +73,19 @@ def get_folios(self, folio_search_param): "id": reservation_line.id, "date": reservation_line.date, "roomId": reservation_line.room_id.id, + "roomName": reservation_line.room_id.name, } ) + segmentation_ids = [] + if reservation.segmentation_ids: + for segmentation in reservation.segmentation_ids: + segmentation_ids.append(segmentation.name) reservations.append( { "id": reservation.id, "name": reservation.name, + "folioSequence": reservation.folio_sequence, "checkin": datetime.combine( reservation.checkin, datetime.min.time() ).isoformat(), @@ -93,6 +99,10 @@ def get_folios(self, folio_search_param): if reservation.room_type_id else "", "priceTotal": reservation.price_total, + "priceRoomServicesSet": reservation.price_room_services_set, + "discount": reservation.discount, + "commission": reservation.agency_id.default_commission, + "partnerName": reservation.partner_name, "adults": reservation.adults, "pricelist": reservation.pricelist_id.name, "boardService": ( @@ -109,12 +119,36 @@ def get_folios(self, folio_search_param): "saleChannel": reservation.channel_type_id.name if reservation.channel_type_id else "", + "externalReference": reservation.external_reference + if reservation.external_reference + else "", "agency": reservation.agency_id.name if reservation.agency_id else "", "agencyImage": reservation.agency_id.image_1024.decode("utf-8") if reservation.agency_id else "", + "state": reservation.state if reservation.state else "", + "roomTypeCode": reservation.room_type_id.default_code + if reservation.room_type_id + else "", + "children": reservation.children + if reservation.children + else "", + "countServices": len(reservation.service_ids) + if reservation.service_ids + else 0, + "readyForCheckin": reservation.ready_for_checkin, + "allowedCheckout": reservation.allowed_checkout, + "isSplitted": reservation.splitted, + "arrivalHour": reservation.arrival_hour, + "departureHour": reservation.departure_hour, + "pendingCheckinData": reservation.pending_checkin_data, + "createDate": reservation.create_date, + "segmentations": segmentation_ids, + "cancellationPolicy": reservation.pricelist_id.cancelation_rule_id.name + if reservation.pricelist_id.cancelation_rule_id.name + else "", } ) result_folios.append( From 07ea46079f3aaf8f82e586aa709e7fd0ef792ea9 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 23 Jun 2022 17:43:28 +0200 Subject: [PATCH 094/547] [IMP] pms-api-rest: add out of range folios to folio service response --- pms_api_rest/services/pms_folio_service.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ba887e5354..70087abeec 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -34,8 +34,13 @@ def get_folios(self, folio_search_param): ) if folio_search_param.date_to and folio_search_param.date_from: - domain_fields.append(("checkin", ">=", folio_search_param.date_from)) - domain_fields.append(("checkout", "<", folio_search_param.date_to)) + reservation_lines = self.env["pms.reservation.line"].search( + [ + ("date", ">=", folio_search_param.date_from), + ("date", "<", folio_search_param.date_to), + ] + ).mapped("reservation_id").mapped("folio_id").ids + domain_fields.append(("folio_id", "in", reservation_lines)) domain_filter = list() if folio_search_param.filter: @@ -149,6 +154,9 @@ def get_folios(self, folio_search_param): "cancellationPolicy": reservation.pricelist_id.cancelation_rule_id.name if reservation.pricelist_id.cancelation_rule_id.name else "", + "pendingAmount": reservation.folio_id.pending_amount, + "toAssign": reservation.to_assign, + "reservationType": reservation.reservation_type } ) result_folios.append( From d4b572370420b1a0002dd897a7edbafcf66ad85b Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 23 Jun 2022 19:12:29 +0200 Subject: [PATCH 095/547] [FIX] pms-api-rest: fix precommit --- pms_api_rest/http.py | 3 --- .../services/pms_availability_plan_service.py | 4 ++-- pms_api_rest/services/pms_folio_service.py | 20 ++++++++++++------- .../services/pms_pricelist_service.py | 4 +++- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/pms_api_rest/http.py b/pms_api_rest/http.py index 3e65ed52a7..762d36c674 100644 --- a/pms_api_rest/http.py +++ b/pms_api_rest/http.py @@ -36,7 +36,6 @@ def _handle_exception(self, exception): if isinstance(exception, SessionExpiredException): # we don't want to return the login form as plain html page # we want to raise a proper exception - print("session expired exception") return wrapJsonException(Unauthorized(ustr(exception))) try: return super(HttpRequest, self)._handle_exception(exception) @@ -46,7 +45,6 @@ def _handle_exception(self, exception): NotFound(ustr(e)), include_description=True, extra_info=extra_info ) except (AccessError, AccessDenied) as e: - print("access error / access denied exception") extra_info = getattr(e, "rest_json_info", None) return wrapJsonException( Forbidden(ustr(e)), include_description=True, extra_info=extra_info @@ -60,7 +58,6 @@ def _handle_exception(self, exception): extra_info = getattr(e, "rest_json_info", None) return wrapJsonException(e, include_description=True, extra_info=extra_info) except Unauthorized as e: - print("Unauthorized exception") extra_info = getattr(e, "rest_json_info", None) return ( wrapJsonException(e, include_description=True, extra_info=extra_info), diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index e7483fe0d2..c878b078c7 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -186,7 +186,8 @@ def create_availability_plan_rule( [ ( [ - "//availability-plan-rules/", + "//" + "availability-plan-rules/", ], "PATCH", ) @@ -202,7 +203,6 @@ def write_availability_plan_rule( [ ("availability_plan_id", "=", availability_plan_id), ("id", "=", availability_plan_rule_id), - ] ) if avail_rule: diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 70087abeec..8b01f9a5d5 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -34,12 +34,18 @@ def get_folios(self, folio_search_param): ) if folio_search_param.date_to and folio_search_param.date_from: - reservation_lines = self.env["pms.reservation.line"].search( - [ - ("date", ">=", folio_search_param.date_from), - ("date", "<", folio_search_param.date_to), - ] - ).mapped("reservation_id").mapped("folio_id").ids + reservation_lines = ( + self.env["pms.reservation.line"] + .search( + [ + ("date", ">=", folio_search_param.date_from), + ("date", "<", folio_search_param.date_to), + ] + ) + .mapped("reservation_id") + .mapped("folio_id") + .ids + ) domain_fields.append(("folio_id", "in", reservation_lines)) domain_filter = list() @@ -156,7 +162,7 @@ def get_folios(self, folio_search_param): else "", "pendingAmount": reservation.folio_id.pending_amount, "toAssign": reservation.to_assign, - "reservationType": reservation.reservation_type + "reservationType": reservation.reservation_type, } ) result_folios.append( diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index bbf76713cb..039d739c55 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -183,7 +183,9 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): input_param=Datamodel("pms.pricelist.item.info", is_list=False), auth="jwt_api_pms", ) - def write_pricelist_item(self, pricelist_id, pricelist_item_id, pms_pricelist_item_info): + def write_pricelist_item( + self, pricelist_id, pricelist_item_id, pms_pricelist_item_info + ): product_pricelist_item = self.env["product.pricelist.item"].search( [ From 85eec630e9d42009abbc4a55398f06c0a061adca Mon Sep 17 00:00:00 2001 From: Sara Date: Thu, 12 May 2022 11:46:02 +0200 Subject: [PATCH 096/547] [IMP] pms_api_rest: add room type class service --- pms_api_rest/datamodels/__init__.py | 1 + .../datamodels/pms_room_type_class.py | 17 +++++ pms_api_rest/services/__init__.py | 1 + .../services/pms_room_type_class_service.py | 63 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_room_type_class.py create mode 100644 pms_api_rest/services/pms_room_type_class_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index ecb00454a4..9be95bc02a 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -4,6 +4,7 @@ from . import pms_room from . import pms_room_type +from . import pms_room_type_class from . import pms_reservation 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..dbc60131de --- /dev/null +++ b/pms_api_rest/datamodels/pms_room_type_class.py @@ -0,0 +1,17 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsRoomTypeClassSearchParam(Datamodel): + _name = "pms.room.type.class.search.param" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + pms_property_ids = 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) + pms_property_ids = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 2763228ea7..fdbc2d46ab 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -9,3 +9,4 @@ from . import pms_login_service from . import pms_pricelist_service from . import pms_availability_plan_service +from . import pms_room_type_class_service 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..1d35f7c9b9 --- /dev/null +++ b/pms_api_rest/services/pms_room_type_class_service.py @@ -0,0 +1,63 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsRoomTypeClassService(Component): + _inherit = "base.rest.service" + _name = "pms.room.type.class.service" + _usage = "room-type-class" + _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): + room_type_class_all_properties = self.env["pms.room.type.class"].search( + [("pms_property_ids", "=", False)] + ) + if room_type_class_search_param.pms_property_ids: + room_type_class = set() + for index, prop in enumerate(room_type_class_search_param.pms_property_ids): + room_type_class_with_query_property = self.env[ + "pms.room.type.class" + ].search([("pms_property_ids", "=", prop)]) + if index == 0: + room_type_class = set(room_type_class_with_query_property.ids) + else: + room_type_class = room_type_class.intersection( + set(room_type_class_with_query_property.ids) + ) + room_type_class_total = list( + set(list(room_type_class) + room_type_class_all_properties.ids) + ) + else: + room_type_class_total = list(room_type_class_all_properties.ids) + domain = [ + ("id", "in", room_type_class_total), + ] + + result_room_type_class = [] + PmsRoomTypeClassInfo = self.env.datamodels["pms.room.type.class.info"] + for room in self.env["pms.room.type.class"].search( + domain, + ): + + result_room_type_class.append( + PmsRoomTypeClassInfo( + id=room.id, + name=room.name, + pms_property_ids=room.pms_property_ids.mapped("id"), + ) + ) + return result_room_type_class From aabf4d8fbcb8d86bb18ba1e124740cf8891d252c Mon Sep 17 00:00:00 2001 From: Sara Date: Thu, 12 May 2022 11:49:40 +0200 Subject: [PATCH 097/547] [IMP] pms_api_rest: add ubication service --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_ubication.py | 17 +++++ pms_api_rest/services/__init__.py | 1 + .../services/pms_ubication_service.py | 63 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_ubication.py create mode 100644 pms_api_rest/services/pms_ubication_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 9be95bc02a..9014866892 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -22,3 +22,4 @@ from . import pms_availability_plan from . import pms_availability_plan_rule from . import pms_search_param +from . import pms_ubication diff --git a/pms_api_rest/datamodels/pms_ubication.py b/pms_api_rest/datamodels/pms_ubication.py new file mode 100644 index 0000000000..ad7ae70591 --- /dev/null +++ b/pms_api_rest/datamodels/pms_ubication.py @@ -0,0 +1,17 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsUbicationSearchParam(Datamodel): + _name = "pms.ubication.search.param" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) + pms_property_ids = 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) + pms_property_ids = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index fdbc2d46ab..f5b706756c 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -10,3 +10,4 @@ from . import pms_pricelist_service from . import pms_availability_plan_service from . import pms_room_type_class_service +from . import pms_ubication_service 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..3f23e09588 --- /dev/null +++ b/pms_api_rest/services/pms_ubication_service.py @@ -0,0 +1,63 @@ +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 = "ubication" + _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): + ubication_all_properties = self.env["pms.ubication"].search( + [("pms_property_ids", "=", False)] + ) + if ubication_search_param.pms_property_ids: + ubication = set() + for index, prop in enumerate(ubication_search_param.pms_property_ids): + ubication_with_query_property = self.env[ + "pms.ubication" + ].search([("pms_property_ids", "=", prop)]) + if index == 0: + ubication = set(ubication_with_query_property.ids) + else: + ubication = ubication.intersection( + set(ubication_with_query_property.ids) + ) + ubication_total = list( + set(list(ubication) + ubication_all_properties.ids) + ) + else: + ubication_total = list(ubication_all_properties.ids) + domain = [ + ("id", "in", ubication_total), + ] + + result_ubications = [] + PmsUbicationInfo = self.env.datamodels["pms.ubication.info"] + for room in self.env["pms.ubication"].search( + domain, + ): + + result_ubications.append( + PmsUbicationInfo( + id=room.id, + name=room.name, + pms_property_ids=room.pms_property_ids.mapped("id"), + ) + ) + return result_ubications From cbdfc6c4ad081b1f24a111a45c5e02fc132bac72 Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 18 May 2022 16:37:53 +0200 Subject: [PATCH 098/547] [IMP] pms_api_rest: add room type class in room service --- pms_api_rest/datamodels/pms_room.py | 1 + pms_api_rest/services/pms_room_service.py | 1 + pms_api_rest/services/pms_ubication_service.py | 10 ++++------ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index 15f3bc3786..66903df397 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -17,3 +17,4 @@ class PmsRoomInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 00d4277986..6a7c094ed0 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -51,6 +51,7 @@ def get_rooms(self, room_search_param): roomTypeId=room.room_type_id, capacity=room.capacity, shortName=room.short_name, + roomTypeClassId=room.room_type_id.class_id, ) ) return result_rooms diff --git a/pms_api_rest/services/pms_ubication_service.py b/pms_api_rest/services/pms_ubication_service.py index 3f23e09588..1f03bd5bc5 100644 --- a/pms_api_rest/services/pms_ubication_service.py +++ b/pms_api_rest/services/pms_ubication_service.py @@ -29,18 +29,16 @@ def get_ubications(self, ubication_search_param): if ubication_search_param.pms_property_ids: ubication = set() for index, prop in enumerate(ubication_search_param.pms_property_ids): - ubication_with_query_property = self.env[ - "pms.ubication" - ].search([("pms_property_ids", "=", prop)]) + ubication_with_query_property = self.env["pms.ubication"].search( + [("pms_property_ids", "=", prop)] + ) if index == 0: ubication = set(ubication_with_query_property.ids) else: ubication = ubication.intersection( set(ubication_with_query_property.ids) ) - ubication_total = list( - set(list(ubication) + ubication_all_properties.ids) - ) + ubication_total = list(set(list(ubication) + ubication_all_properties.ids)) else: ubication_total = list(ubication_all_properties.ids) domain = [ From cb23ad20ba68b75eda08a17d3f198bf99004597f Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 18 May 2022 16:54:37 +0200 Subject: [PATCH 099/547] [IMP] pms_api_rest: add ubication in room datamodel and service --- pms_api_rest/datamodels/pms_room.py | 1 + pms_api_rest/services/pms_room_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index 66903df397..987af4c585 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -18,3 +18,4 @@ class PmsRoomInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 6a7c094ed0..9b0d5d2104 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -52,6 +52,7 @@ def get_rooms(self, room_search_param): capacity=room.capacity, shortName=room.short_name, roomTypeClassId=room.room_type_id.class_id, + ubicationId=room.ubication_id, ) ) return result_rooms From 1022bc9a230c1c02e738071e60b3759addc924e2 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 23 Jun 2022 21:39:28 +0200 Subject: [PATCH 100/547] [IMP]pms_api_rest: added services and datamodels for get and patch checkin_partners --- pms_api_rest/__manifest__.py | 1 + pms_api_rest/datamodels/__init__.py | 6 + .../datamodels/pms_checkin_partner.py | 14 +- pms_api_rest/datamodels/pms_id_categories.py | 9 + pms_api_rest/datamodels/res_city_zip.py | 10 + pms_api_rest/datamodels/res_country.py | 15 ++ .../datamodels/res_partner_category.py | 10 + pms_api_rest/services/__init__.py | 4 + pms_api_rest/services/pms_folio_service.py | 12 +- .../services/pms_id_categories_service.py | 34 ++++ pms_api_rest/services/pms_partner_service.py | 84 ++++++++ .../services/pms_reservation_service.py | 189 ++++++++++++++++-- pms_api_rest/services/res_city_zip_service.py | 35 ++++ pms_api_rest/services/res_country_services.py | 60 ++++++ .../services/res_partner_category_services.py | 37 ++++ 15 files changed, 502 insertions(+), 18 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_id_categories.py create mode 100644 pms_api_rest/datamodels/res_city_zip.py create mode 100644 pms_api_rest/datamodels/res_country.py create mode 100644 pms_api_rest/datamodels/res_partner_category.py create mode 100644 pms_api_rest/services/pms_id_categories_service.py create mode 100644 pms_api_rest/services/res_city_zip_service.py create mode 100644 pms_api_rest/services/res_country_services.py create mode 100644 pms_api_rest/services/res_partner_category_services.py diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index f7af4d0ef7..90e77815ad 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -12,6 +12,7 @@ "web", "auth_signup", "auth_jwt_login", + "base_location", ], "external_dependencies": { "python": ["jwt", "simplejson", "marshmallow", "jose"], diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 9014866892..429799837f 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -21,5 +21,11 @@ from . import pms_pricelist_item from . import pms_availability_plan from . import pms_availability_plan_rule + +from . import pms_id_categories +from . import res_country +from . import res_partner_category +from . import res_city_zip + from . import pms_search_param from . import pms_ubication diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py index 74df84b0a4..b34466e3e0 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner.py +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -6,13 +6,21 @@ class PmsCheckinPartnerInfo(Datamodel): _name = "pms.checkin.partner.info" id = fields.Integer(required=False, allow_none=True) - # partner = fields.String(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) - nationality = fields.String(required=False, allow_none=True) documentType = fields.String(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) gender = fields.String(required=False, allow_none=True) - state = 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.String(required=False, allow_none=True) + countryState = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_id_categories.py b/pms_api_rest/datamodels/pms_id_categories.py new file mode 100644 index 0000000000..09dec38c1a --- /dev/null +++ b/pms_api_rest/datamodels/pms_id_categories.py @@ -0,0 +1,9 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsIdCategoriesInfo(Datamodel): + _name = "pms.id.categories.info" + id = fields.Integer(required=False, allow_none=True) + documentType = 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..aca9f2d123 --- /dev/null +++ b/pms_api_rest/datamodels/res_city_zip.py @@ -0,0 +1,10 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class ResCityZipInfo(Datamodel): + _name = "res.city.zip.info" + cityId = fields.String(required=False, allow_none=True) + stateId = fields.String(required=False, allow_none=True) + countryId = 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..d584be8c2b --- /dev/null +++ b/pms_api_rest/datamodels/res_country.py @@ -0,0 +1,15 @@ +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) + + +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_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/services/__init__.py b/pms_api_rest/services/__init__.py index f5b706756c..694726f087 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -9,5 +9,9 @@ from . import pms_login_service from . import pms_pricelist_service from . import pms_availability_plan_service +from . import pms_id_categories_service +from . import res_country_services +from . import res_partner_category_services +from . import res_city_zip_service from . import pms_room_type_class_service from . import pms_ubication_service diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 8b01f9a5d5..dbd99b8280 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -106,7 +106,13 @@ def get_folios(self, folio_search_param): "preferredRoomId": reservation.preferred_room_id.name if reservation.preferred_room_id else "", - "roomTypeId": reservation.room_type_id.name + "preferredRoomCapacity": reservation.preferred_room_id.capacity + if reservation.preferred_room_id + else "", + "roomTypeName": reservation.room_type_id.name + if reservation.room_type_id + else "", + "roomTypeId": reservation.room_type_id.id if reservation.room_type_id else "", "priceTotal": reservation.price_total, @@ -156,7 +162,9 @@ def get_folios(self, folio_search_param): "departureHour": reservation.departure_hour, "pendingCheckinData": reservation.pending_checkin_data, "createDate": reservation.create_date, - "segmentations": segmentation_ids, + "segmentations": segmentation_ids[0] + if segmentation_ids + else "", "cancellationPolicy": reservation.pricelist_id.cancelation_rule_id.name if reservation.pricelist_id.cancelation_rule_id.name else "", diff --git a/pms_api_rest/services/pms_id_categories_service.py b/pms_api_rest/services/pms_id_categories_service.py new file mode 100644 index 0000000000..1a7a13e150 --- /dev/null +++ b/pms_api_rest/services/pms_id_categories_service.py @@ -0,0 +1,34 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsIdCategoriesService(Component): + _inherit = "base.rest.service" + _name = "pms.id.categories.services" + _usage = "id_categories" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.id.categories.info", is_list=True), + auth="jwt_api_pms", + ) + def get_id_categories(self): + result_id_categories = [] + PmsIdCategoriesInfo = self.env.datamodels["pms.id.categories.info"] + for id_category in self.env["res.partner.id_category"].search([]): + result_id_categories.append( + PmsIdCategoriesInfo( + id=id_category.id, + documentType=id_category.name, + ) + ) + return result_id_categories diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 38f5eba88e..db2b526916 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -36,3 +36,87 @@ def get_partners(self): ) ) return result_partners + + @restapi.method( + [ + ( + [ + "//", + ], + "GET", + ) + ], + output_param=Datamodel("pms.partner.info", is_list=True), + auth="jwt_api_pms", + ) + def get_partner_by_doc_number(self, document_type, document_number): + doc_type = self.env["res.partner.id_category"].search( + [("name", "=", document_type)] + ) + doc_number = self.env["res.partner.id_number"].search( + [("name", "=", document_number), ("category_id", "=", doc_type.id)] + ) + partners = [] + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] + if not doc_number: + pass + else: + if doc_number.valid_from: + document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") + if doc_number.partner_id.birthdate_date: + birthdate_date = doc_number.partner_id.birthdate_date.strftime( + "%d/%m/%Y" + ) + partners.append( + PmsCheckinPartnerInfo( + # id=doc_number.partner_id.id, + name=doc_number.partner_id.name + if doc_number.partner_id.name + else "", + firstname=doc_number.partner_id.firstname + if doc_number.partner_id.firstname + else "", + lastname=doc_number.partner_id.lastname + if doc_number.partner_id.lastname + else "", + lastname2=doc_number.partner_id.lastname2 + if doc_number.partner_id.lastname2 + else "", + email=doc_number.partner_id.email + if doc_number.partner_id.email + else "", + mobile=doc_number.partner_id.mobile + if doc_number.partner_id.mobile + else "", + documentType=doc_type.name, + documentNumber=doc_number.name, + documentExpeditionDate=document_expedition_date + if doc_number.valid_from + else "", + documentSupportNumber=doc_number.support_number + if doc_number.support_number + else "", + gender=doc_number.partner_id.gender + if doc_number.partner_id.gender + else "", + birthdate=birthdate_date + if doc_number.partner_id.birthdate_date + else "", + residenceStreet=doc_number.partner_id.residence_street + if doc_number.partner_id.residence_street + else "", + zip=doc_number.partner_id.residence_zip + if doc_number.partner_id.residence_zip + else "", + residenceCity=doc_number.partner_id.residence_city + if doc_number.partner_id.residence_city + else "", + nationality=doc_number.partner_id.nationality_id.name + if doc_number.partner_id.nationality_id + else "", + countryState=doc_number.partner_id.residence_state_id.name + if doc_number.partner_id.residence_state_id + else "", + ) + ) + return partners diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 4e7c11757e..40543cda65 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -204,41 +204,204 @@ def update_reservation(self, reservation_id, reservation_lines_changes): "GET", ) ], - input_param=Datamodel("pms.search.param"), output_param=Datamodel("pms.checkin.partner.info", is_list=True), auth="jwt_api_pms", ) - def get_checkin_partners(self, reservation_id, pms_search_param): - domain = list() - domain.append(("id", "=", reservation_id)) - domain.append(("pms_property_id", "=", pms_search_param.pms_property_id)) - reservation = self.env["pms.reservation"].search(domain) + 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: for checkin_partner in reservation.checkin_partner_ids: + if checkin_partner.document_expedition_date: + document_expedition_date = ( + checkin_partner.document_expedition_date.strftime("%d/%m/%Y") + ) + if checkin_partner.birthdate_date: + birthdate_date = checkin_partner.birthdate_date.strftime("%d/%m/%Y") 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 "", + lastname=checkin_partner.lastname + if checkin_partner.lastname + else "", + lastname2=checkin_partner.lastname2 + if checkin_partner.lastname2 + else "", email=checkin_partner.email if checkin_partner.email else "", mobile=checkin_partner.mobile if checkin_partner.mobile else "", - nationality=checkin_partner.nationality_id.name - if checkin_partner.nationality_id - else "", documentType=checkin_partner.document_type.name - if checkin_partner.document_type + if checkin_partner.document_type.name else "", documentNumber=checkin_partner.document_number if checkin_partner.document_number else "", + documentExpeditionDate=document_expedition_date + if checkin_partner.document_expedition_date + else "", + documentSupportNumber=checkin_partner.support_number + if checkin_partner.support_number + else "", gender=checkin_partner.gender if checkin_partner.gender else "", - state=dict( - checkin_partner.fields_get(["state"])["state"]["selection"] - )[checkin_partner.state], + birthdate=birthdate_date + if checkin_partner.birthdate_date + else "", + residenceStreet=checkin_partner.residence_street + if checkin_partner.residence_street + else "", + zip=checkin_partner.residence_zip + if checkin_partner.residence_zip + else "", + residenceCity=checkin_partner.residence_city + if checkin_partner.residence_city + else "", + nationality=checkin_partner.residence_country_id.name + if checkin_partner.residence_country_id + else "", + countryState=checkin_partner.residence_state_id.name + if checkin_partner.residence_state_id + else "", ) ) return checkin_partners + + @restapi.method( + [ + ( + [ + "//adults/", + ], + "PATCH", + ) + ], + auth="jwt_api_pms", + ) + def update_reservation_adults(self, reservation_id, adults): + reservation_id = self.env["pms.reservation"].browse(reservation_id) + reservation_id.write( + { + "adults": adults, + } + ) + + @restapi.method( + [ + ( + [ + "//children/", + ], + "PATCH", + ) + ], + auth="jwt_api_pms", + ) + def update_reservation_children(self, reservation_id, children): + reservation_id = self.env["pms.reservation"].browse(reservation_id) + reservation_id.write( + { + "children": children, + } + ) + + @restapi.method( + [ + ( + [ + "//segmentation/", + ], + "PATCH", + ) + ], + auth="jwt_api_pms", + ) + def update_segmentation_id(self, reservation_id, segmentation_id): + reservation_id = self.env["pms.reservation"].browse(reservation_id) + reservation_id.write( + { + "segmentation_ids": [segmentation_id], + } + ) + + @restapi.method( + [ + ( + [ + "//checkin_partner/", + ], + "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 checkin_partner: + vals = dict() + if pms_checkin_partner_info.firstname: + vals.update({"firstname": pms_checkin_partner_info.firstname}) + if pms_checkin_partner_info.lastname: + vals.update({"lastname": pms_checkin_partner_info.lastname}) + if pms_checkin_partner_info.lastname2: + vals.update({"lastname2": pms_checkin_partner_info.lastname2}) + if pms_checkin_partner_info.email: + vals.update({"email": pms_checkin_partner_info.email}) + if pms_checkin_partner_info.mobile: + vals.update({"mobile": pms_checkin_partner_info.mobile}) + if pms_checkin_partner_info.documentType: + document_type = self.env["res.partner.id_category"].search( + [("name", "=", pms_checkin_partner_info.documentType)] + ) + vals.update({"document_type": document_type}) + if pms_checkin_partner_info.documentNumber: + vals.update( + {"document_number": pms_checkin_partner_info.documentNumber} + ) + 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}) + if pms_checkin_partner_info.documentSupportNumber: + vals.update( + {"support_number": pms_checkin_partner_info.documentSupportNumber} + ) + if pms_checkin_partner_info.gender: + vals.update({"gender": pms_checkin_partner_info.gender}) + 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}) + if pms_checkin_partner_info.residenceStreet: + vals.update( + {"residence_street": pms_checkin_partner_info.residenceStreet} + ) + if pms_checkin_partner_info.zip: + vals.update({"residence_zip": pms_checkin_partner_info.zip}) + if pms_checkin_partner_info.residenceCity: + vals.update({"residence_city": pms_checkin_partner_info.residenceCity}) + if pms_checkin_partner_info.nationality: + country_id = self.env["res.country"].search( + [("name", "=", pms_checkin_partner_info.nationality)] + ) + vals.update({"residence_country_id": country_id}) + if pms_checkin_partner_info.countryState: + country_state = self.env["res.country.state"].search( + [("name", "=", pms_checkin_partner_info.countryState)] + ) + vals.update({"residence_state_id": country_state}) + checkin_partner.write(vals) 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..12efe97be1 --- /dev/null +++ b/pms_api_rest/services/res_city_zip_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 PmsIdCategoriesService(Component): + _inherit = "base.rest.service" + _name = "res.city.zip.services" + _usage = "zip" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("res.city.zip.info", is_list=True), + auth="jwt_api_pms", + ) + def get_address_data_by_zip(self, res_city_zip): + result_zip_data = [] + ResCityZipInfo = self.env.datamodels["res.city.zip.info"] + for zip_code in self.env["res.city.zip"].search([("name", "=", res_city_zip)]): + result_zip_data.append( + ResCityZipInfo( + cityId=zip_code.city_id.name, + stateId=zip_code.state_id.name, + countryId=zip_code.country_id.name, + ) + ) + return result_zip_data diff --git a/pms_api_rest/services/res_country_services.py b/pms_api_rest/services/res_country_services.py new file mode 100644 index 0000000000..6236bc9c86 --- /dev/null +++ b/pms_api_rest/services/res_country_services.py @@ -0,0 +1,60 @@ +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.services" + _usage = "countries" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("res.country.info", is_list=True), + auth="jwt_api_pms", + ) + def get_countries(self): + result_countries = [] + ResCountriesInfo = self.env.datamodels["res.country.info"] + for country in self.env["res.country"].search([]): + result_countries.append( + ResCountriesInfo( + id=country.id, + name=country.name, + ) + ) + return result_countries + + @restapi.method( + [ + ( + [ + "//country_states", + ], + "GET", + ) + ], + output_param=Datamodel("res.country_state.info", is_list=True), + auth="jwt_api_pms", + ) + 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"].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_partner_category_services.py b/pms_api_rest/services/res_partner_category_services.py new file mode 100644 index 0000000000..8b99521fd8 --- /dev/null +++ b/pms_api_rest/services/res_partner_category_services.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 PmsIdCategoriesService(Component): + _inherit = "base.rest.service" + _name = "res.partner.category.services" + _usage = "segmentations" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("res.partner.category.info", is_list=True), + auth="jwt_api_pms", + ) + def get_parent_segmentation_ids(self): + result_segmentation_ids = [] + ResPartnerCategoryInfo = self.env.datamodels["res.partner.category.info"] + for segmentation_id in self.env["res.partner.category"].search([]): + result_segmentation_ids.append( + ResPartnerCategoryInfo( + id=segmentation_id.id, + name=segmentation_id.name, + parentId=segmentation_id.parent_id.id + if segmentation_id.parent_id.id + else 0, + ) + ) + return result_segmentation_ids From a3296c34220650a392c69fa6fb3978d9a369a5d2 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 5 Jul 2022 18:04:57 +0200 Subject: [PATCH 101/547] [IMP]pms_api_rest: added services and datamodels for save checkin partners data --- pms_api_rest/datamodels/pms_calendar.py | 3 + .../datamodels/pms_checkin_partner.py | 7 +- pms_api_rest/services/pms_folio_service.py | 10 +- pms_api_rest/services/pms_partner_service.py | 12 +- .../services/pms_reservation_service.py | 227 +++++++++--------- 5 files changed, 135 insertions(+), 124 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index d539f2448b..9391864e7e 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -9,6 +9,9 @@ class PmsReservationUpdates(Datamodel): 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): diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py index b34466e3e0..045cc27e23 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner.py +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -13,7 +13,7 @@ class PmsCheckinPartnerInfo(Datamodel): lastname2 = fields.String(required=False, allow_none=True) email = fields.String(required=False, allow_none=True) mobile = fields.String(required=False, allow_none=True) - documentType = 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) @@ -22,5 +22,6 @@ class PmsCheckinPartnerInfo(Datamodel): 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.String(required=False, allow_none=True) - countryState = fields.String(required=False, allow_none=True) + nationality = fields.Integer(required=False, allow_none=True) + countryState = fields.Integer(required=False, allow_none=True) + checkinPartnerState = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index dbd99b8280..6742a4b3c7 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -90,7 +90,7 @@ def get_folios(self, folio_search_param): segmentation_ids = [] if reservation.segmentation_ids: for segmentation in reservation.segmentation_ids: - segmentation_ids.append(segmentation.name) + segmentation_ids.append(segmentation.id) reservations.append( { @@ -149,9 +149,7 @@ def get_folios(self, folio_search_param): "roomTypeCode": reservation.room_type_id.default_code if reservation.room_type_id else "", - "children": reservation.children - if reservation.children - else "", + "children": reservation.children if reservation.children else 0, "countServices": len(reservation.service_ids) if reservation.service_ids else 0, @@ -162,9 +160,9 @@ def get_folios(self, folio_search_param): "departureHour": reservation.departure_hour, "pendingCheckinData": reservation.pending_checkin_data, "createDate": reservation.create_date, - "segmentations": segmentation_ids[0] + "segmentationId": segmentation_ids[0] if segmentation_ids - else "", + else 0, "cancellationPolicy": reservation.pricelist_id.cancelation_rule_id.name if reservation.pricelist_id.cancelation_rule_id.name else "", diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index db2b526916..2a95e6af07 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -51,7 +51,7 @@ def get_partners(self): ) def get_partner_by_doc_number(self, document_type, document_number): doc_type = self.env["res.partner.id_category"].search( - [("name", "=", document_type)] + [("id", "=", document_type)] ) doc_number = self.env["res.partner.id_number"].search( [("name", "=", document_number), ("category_id", "=", doc_type.id)] @@ -88,7 +88,7 @@ def get_partner_by_doc_number(self, document_type, document_number): mobile=doc_number.partner_id.mobile if doc_number.partner_id.mobile else "", - documentType=doc_type.name, + documentType=doc_type.id, documentNumber=doc_number.name, documentExpeditionDate=document_expedition_date if doc_number.valid_from @@ -111,12 +111,12 @@ def get_partner_by_doc_number(self, document_type, document_number): residenceCity=doc_number.partner_id.residence_city if doc_number.partner_id.residence_city else "", - nationality=doc_number.partner_id.nationality_id.name + nationality=doc_number.partner_id.nationality_id.id if doc_number.partner_id.nationality_id - else "", - countryState=doc_number.partner_id.residence_state_id.name + else -1, + countryState=doc_number.partner_id.residence_state_id.id if doc_number.partner_id.residence_state_id - else "", + else -1, ) ) return partners diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 40543cda65..8d65f4cbbe 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -192,7 +192,20 @@ def update_reservation(self, reservation_id, reservation_lines_changes): 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: + 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) @restapi.method( @@ -214,7 +227,10 @@ def get_checkin_partners(self, reservation_id): if not reservation: pass else: - for checkin_partner in reservation.checkin_partner_ids: + # TODO Review state draft + for checkin_partner in reservation.checkin_partner_ids.filtered( + lambda ch: ch.state != "dummy" + ): if checkin_partner.document_expedition_date: document_expedition_date = ( checkin_partner.document_expedition_date.strftime("%d/%m/%Y") @@ -237,9 +253,9 @@ def get_checkin_partners(self, reservation_id): else "", email=checkin_partner.email if checkin_partner.email else "", mobile=checkin_partner.mobile if checkin_partner.mobile else "", - documentType=checkin_partner.document_type.name - if checkin_partner.document_type.name - else "", + documentType=checkin_partner.document_type.id + if checkin_partner.document_type + else -1, documentNumber=checkin_partner.document_number if checkin_partner.document_number else "", @@ -262,12 +278,13 @@ def get_checkin_partners(self, reservation_id): residenceCity=checkin_partner.residence_city if checkin_partner.residence_city else "", - nationality=checkin_partner.residence_country_id.name + nationality=checkin_partner.residence_country_id.id if checkin_partner.residence_country_id - else "", - countryState=checkin_partner.residence_state_id.name + else -1, + countryState=checkin_partner.residence_state_id.id if checkin_partner.residence_state_id - else "", + else -1, + checkinPartnerState=checkin_partner.state, ) ) return checkin_partners @@ -276,132 +293,124 @@ def get_checkin_partners(self, reservation_id): [ ( [ - "//adults/", + "//checkin_partners/", ], "PATCH", ) ], + input_param=Datamodel("pms.checkin.partner.info", is_list=False), auth="jwt_api_pms", ) - def update_reservation_adults(self, reservation_id, adults): - reservation_id = self.env["pms.reservation"].browse(reservation_id) - reservation_id.write( - { - "adults": adults, - } + 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 checkin_partner: + checkin_partner.write( + self._get_checkin_partner_values(pms_checkin_partner_info) + ) @restapi.method( [ ( [ - "//children/", + "//checkin_partners", ], - "PATCH", + "POST", ) ], + input_param=Datamodel("pms.checkin.partner.info", is_list=False), auth="jwt_api_pms", ) - def update_reservation_children(self, reservation_id, children): - reservation_id = self.env["pms.reservation"].browse(reservation_id) - reservation_id.write( - { - "children": children, - } - ) + 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._get_checkin_partner_values(pms_checkin_partner_info) + ) @restapi.method( [ ( [ - "//segmentation/", + "//checkin_partners/", ], - "PATCH", + "DELETE", ) ], auth="jwt_api_pms", ) - def update_segmentation_id(self, reservation_id, segmentation_id): - reservation_id = self.env["pms.reservation"].browse(reservation_id) - reservation_id.write( - { - "segmentation_ids": [segmentation_id], - } - ) + def delete_reservation_checkin_partner(self, reservation_id, checkin_partner_id): + reservation = self.env["pms.reservation"].browse(reservation_id) + reservation.adults = reservation.adults - 1 - @restapi.method( - [ - ( - [ - "//checkin_partner/", - ], - "PATCH", + def _get_checkin_partner_values(self, pms_checkin_partner_info): + vals = dict() + if pms_checkin_partner_info.firstname: + vals.update({"firstname": pms_checkin_partner_info.firstname}) + if pms_checkin_partner_info.lastname: + vals.update({"lastname": pms_checkin_partner_info.lastname}) + if pms_checkin_partner_info.lastname2: + vals.update({"lastname2": pms_checkin_partner_info.lastname2}) + if pms_checkin_partner_info.email: + vals.update({"email": pms_checkin_partner_info.email}) + if pms_checkin_partner_info.mobile: + vals.update({"mobile": pms_checkin_partner_info.mobile}) + if ( + pms_checkin_partner_info.documentType + and pms_checkin_partner_info.documentType != -1 + ): + document_type = pms_checkin_partner_info.documentType + vals.update({"document_type": document_type}) + if pms_checkin_partner_info.documentNumber: + vals.update({"document_number": pms_checkin_partner_info.documentNumber}) + if pms_checkin_partner_info.documentExpeditionDate: + document_expedition_date = datetime.strptime( + pms_checkin_partner_info.documentExpeditionDate, "%d/%m/%Y" ) - ], - 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 checkin_partner: - vals = dict() - if pms_checkin_partner_info.firstname: - vals.update({"firstname": pms_checkin_partner_info.firstname}) - if pms_checkin_partner_info.lastname: - vals.update({"lastname": pms_checkin_partner_info.lastname}) - if pms_checkin_partner_info.lastname2: - vals.update({"lastname2": pms_checkin_partner_info.lastname2}) - if pms_checkin_partner_info.email: - vals.update({"email": pms_checkin_partner_info.email}) - if pms_checkin_partner_info.mobile: - vals.update({"mobile": pms_checkin_partner_info.mobile}) - if pms_checkin_partner_info.documentType: - document_type = self.env["res.partner.id_category"].search( - [("name", "=", pms_checkin_partner_info.documentType)] - ) - vals.update({"document_type": document_type}) - if pms_checkin_partner_info.documentNumber: - vals.update( - {"document_number": pms_checkin_partner_info.documentNumber} - ) - 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}) - if pms_checkin_partner_info.documentSupportNumber: - vals.update( - {"support_number": pms_checkin_partner_info.documentSupportNumber} - ) - if pms_checkin_partner_info.gender: - vals.update({"gender": pms_checkin_partner_info.gender}) - 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}) - if pms_checkin_partner_info.residenceStreet: - vals.update( - {"residence_street": pms_checkin_partner_info.residenceStreet} - ) - if pms_checkin_partner_info.zip: - vals.update({"residence_zip": pms_checkin_partner_info.zip}) - if pms_checkin_partner_info.residenceCity: - vals.update({"residence_city": pms_checkin_partner_info.residenceCity}) - if pms_checkin_partner_info.nationality: - country_id = self.env["res.country"].search( - [("name", "=", pms_checkin_partner_info.nationality)] - ) - vals.update({"residence_country_id": country_id}) - if pms_checkin_partner_info.countryState: - country_state = self.env["res.country.state"].search( - [("name", "=", pms_checkin_partner_info.countryState)] - ) - vals.update({"residence_state_id": country_state}) - checkin_partner.write(vals) + document_expedition_date = document_expedition_date.strftime("%Y-%m-%d") + vals.update({"document_expedition_date": document_expedition_date}) + if pms_checkin_partner_info.documentSupportNumber: + vals.update( + {"support_number": pms_checkin_partner_info.documentSupportNumber} + ) + if pms_checkin_partner_info.gender: + vals.update({"gender": pms_checkin_partner_info.gender}) + 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}) + if pms_checkin_partner_info.residenceStreet: + vals.update({"residence_street": pms_checkin_partner_info.residenceStreet}) + if pms_checkin_partner_info.zip: + vals.update({"residence_zip": pms_checkin_partner_info.zip}) + if pms_checkin_partner_info.residenceCity: + vals.update({"residence_city": pms_checkin_partner_info.residenceCity}) + if ( + pms_checkin_partner_info.nationality + and pms_checkin_partner_info.nationality != -1 + ): + vals.update({"nationality_id": pms_checkin_partner_info.nationality}) + vals.update({"residence_country_id": pms_checkin_partner_info.nationality}) + if ( + pms_checkin_partner_info.countryState + and pms_checkin_partner_info.countryState != -1 + ): + vals.update({"residence_state_id": pms_checkin_partner_info.countryState}) + return vals From 228eac4bb3cc46ed845e95a7d662d8dfd65e1eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 15 Jun 2022 12:50:17 +0200 Subject: [PATCH 102/547] [ADD]pms_api_rest: extra beds service --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_extra_bed.py | 20 ++++++ pms_api_rest/services/__init__.py | 1 + .../services/pms_extra_beds_service.py | 72 +++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_extra_bed.py create mode 100644 pms_api_rest/services/pms_extra_beds_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 429799837f..8f5e0178dc 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -29,3 +29,4 @@ from . import pms_search_param from . import pms_ubication +from . import pms_extra_bed 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..9cfc9f7f22 --- /dev/null +++ b/pms_api_rest/datamodels/pms_extra_bed.py @@ -0,0 +1,20 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsExtraBedSearchParam(Datamodel): + _name = "pms.extra.beds.search.param" + id = fields.Integer(required=False, allow_none=True) + 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/services/__init__.py b/pms_api_rest/services/__init__.py index 694726f087..14419c3446 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -15,3 +15,4 @@ from . import res_city_zip_service from . import pms_room_type_class_service from . import pms_ubication_service +from . import pms_extra_beds_service diff --git a/pms_api_rest/services/pms_extra_beds_service.py b/pms_api_rest/services/pms_extra_beds_service.py new file mode 100644 index 0000000000..ad6d758267 --- /dev/null +++ b/pms_api_rest/services/pms_extra_beds_service.py @@ -0,0 +1,72 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsExtraBedsService(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.id: + domain.append(("id", "=", extra_beds_search_param.id)) + 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) + 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 From 08fd633e4929caff4b8847db75b0756278d97388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 15 Jun 2022 16:50:23 +0200 Subject: [PATCH 103/547] [FIX]pms_api_rest: extra beds not get service lines --- pms_api_rest/services/pms_extra_beds_service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_extra_beds_service.py b/pms_api_rest/services/pms_extra_beds_service.py index ad6d758267..a5bfca4326 100644 --- a/pms_api_rest/services/pms_extra_beds_service.py +++ b/pms_api_rest/services/pms_extra_beds_service.py @@ -55,7 +55,10 @@ def get_extra_beds(self, extra_beds_search_param): ["day_qty:sum"], ["date:day"], ) - max_daily_used = max(date["day_qty"] for date in qty_for_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 From e6767e5858a91bb582864fbca930cdea7b5bfefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 15 Jun 2022 16:59:17 +0200 Subject: [PATCH 104/547] [IMP] pms_api_rest: add room extra beds allowed --- pms_api_rest/datamodels/pms_room.py | 1 + pms_api_rest/services/pms_room_service.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index 987af4c585..6413634d34 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -19,3 +19,4 @@ class PmsRoomInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 9b0d5d2104..c1dadb5b7d 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -53,6 +53,7 @@ def get_rooms(self, room_search_param): shortName=room.short_name, roomTypeClassId=room.room_type_id.class_id, ubicationId=room.ubication_id, + extraBedsAllowed=room.extra_beds_allowed, ) ) return result_rooms @@ -79,6 +80,7 @@ def get_room(self, room_id): roomTypeId=room.room_type_id, capacity=room.capacity, shortName=room.short_name, + extraBedsAllowed=room.extra_beds_allowed, ) else: raise MissingError(_("Room not found")) From 4b7c87d07bdf22cb71dba51e1f11d31ec6fe64bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 16 Jun 2022 11:21:34 +0200 Subject: [PATCH 105/547] [ADD]pms_api_rest: amenities and amenity types services --- pms_api_rest/datamodels/__init__.py | 3 + pms_api_rest/datamodels/pms_amenity.py | 18 ++++ pms_api_rest/datamodels/pms_amenity_type.py | 17 ++++ pms_api_rest/datamodels/pms_room.py | 1 + pms_api_rest/services/__init__.py | 3 + pms_api_rest/services/pms_amenity_service.py | 82 ++++++++++++++++++ .../services/pms_amenity_type_service.py | 86 +++++++++++++++++++ 7 files changed, 210 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_amenity.py create mode 100644 pms_api_rest/datamodels/pms_amenity_type.py create mode 100644 pms_api_rest/services/pms_amenity_service.py create mode 100644 pms_api_rest/services/pms_amenity_type_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 8f5e0178dc..34616fd3f5 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -30,3 +30,6 @@ from . import pms_search_param from . import pms_ubication from . import pms_extra_bed + +from . import pms_amenity_type +from . import pms_amenity diff --git a/pms_api_rest/datamodels/pms_amenity.py b/pms_api_rest/datamodels/pms_amenity.py new file mode 100644 index 0000000000..f8bfe33968 --- /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" + id = fields.Integer(required=False, allow_none=True) + 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) + pmsAmenityTypeId = fields.Integer(required=True, allow_none=False) 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..dacbf95503 --- /dev/null +++ b/pms_api_rest/datamodels/pms_amenity_type.py @@ -0,0 +1,17 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAmenityTypeSearchParam(Datamodel): + _name = "pms.amenity.type.search.param" + id = fields.Integer(required=False, allow_none=True) + 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) + pmsAmenityIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index 6413634d34..fffc82ca5b 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -20,3 +20,4 @@ class PmsRoomInfo(Datamodel): 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) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 14419c3446..cff288066b 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -16,3 +16,6 @@ from . import pms_room_type_class_service from . import pms_ubication_service from . import pms_extra_beds_service + +from . import pms_amenity_service +from . import pms_amenity_type_service 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..edca83c729 --- /dev/null +++ b/pms_api_rest/services/pms_amenity_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 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 = [] + if amenities_search_param.name: + domain.append(("name", "like", amenities_search_param.name)) + if amenities_search_param.id: + domain.append(("id", "=", amenities_search_param.id)) + 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, + defaultCode=amenity.default_code, + pmsAmenityTypeId=amenity.pms_amenity_type_id.id, + ) + ) + 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, + pmsAmenityTypeId=amenity.pms_amenity_type_id.id, + ) + 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..999d4b5b5d --- /dev/null +++ b/pms_api_rest/services/pms_amenity_type_service.py @@ -0,0 +1,86 @@ +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.id: + domain.append(("id", "=", amenity_types_search_param.id)) + 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, + pmsAmenityIds=amenity_type.pms_amenity_ids.ids, + ) + ) + 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, + pmsAmenityIds=amenity_type.pms_amenity_ids.ids, + ) + else: + raise MissingError(_("Amenity Type not found")) From 0c042f25028094a7ac9fc92c20865d7ae8eaeb6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 20 Jun 2022 18:48:31 +0200 Subject: [PATCH 106/547] [ADD]pms_api_rest: reservation_lines get and patch services --- pms_api_rest/datamodels/__init__.py | 1 + .../datamodels/pms_reservation_line.py | 23 +++ pms_api_rest/services/__init__.py | 1 + .../services/pms_reservation_line_service.py | 133 ++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_reservation_line.py create mode 100644 pms_api_rest/services/pms_reservation_line_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 34616fd3f5..199a2eab6b 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -7,6 +7,7 @@ from . import pms_room_type_class from . import pms_reservation +from . import pms_reservation_line from . import pms_checkin_partner from . import pms_partner 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..22f770e01c --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation_line.py @@ -0,0 +1,23 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsReservationLineSearchParam(Datamodel): + _name = "pms.reservation.line.search.param" + id = fields.Integer(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + reservationId = fields.Integer(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + + +class PmsReservationLineInfo(Datamodel): + _name = "pms.reservation.line.info" + id = fields.Integer(required=False, allow_none=False) + date = fields.String(required=False, allow_none=False) + price = fields.Float(required=False, allow_none=False) + discount = fields.Float(required=False, allow_none=True) + cancelDiscount = fields.Float(required=False, allow_none=True) + roomId = fields.Integer(required=False, allow_none=False) + reservationId = fields.Integer(required=False, allow_none=False) + pmsPropertyId = fields.Integer(required=False, allow_none=False) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index cff288066b..fc97b6bb8b 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -5,6 +5,7 @@ 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 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..56c51e37ff --- /dev/null +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -0,0 +1,133 @@ +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=reservation_line.price, + discount=reservation_line.discount, + cancelDiscount=reservation_line.cancel_discount, + roomId=reservation_line.room_id.id, + reservationId=reservation_line.reservation_id.id, + pmsPropertyId=reservation_line.pms_property_id.id, + ) + else: + raise MissingError(_("Reservation Line not found")) + + @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.id: + domain.append(("id", "=", reservation_lines_search_param.id)) + 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=reservation_line.price, + discount=reservation_line.discount, + cancelDiscount=reservation_line.cancel_discount, + roomId=reservation_line.room_id.id, + reservationId=reservation_line.reservation_id.id, + pmsPropertyId=reservation_line.pms_property_id.id, + ) + ) + return result_lines + + @restapi.method( + [ + ( + [ + "/", + ], + "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: + vals["price"] = reservation_line_info.price + if reservation_line_info.discount: + vals["discount"] = reservation_line_info.discount + if reservation_line_info.cancelDiscount: + vals["cancel_discount"] = reservation_line_info.cancelDiscount + if reservation_line_info.roomId: + vals["room_id"] = reservation_line_info.roomId + reservation_line.write(vals) + else: + raise MissingError(_("Reservation Line not found")) From b9a5b1dcf1486cb3cd4cb57865378bd999d3896a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 20 Jun 2022 18:50:42 +0200 Subject: [PATCH 107/547] [DEL]pms_api_rest: del amenityIds from amenity type datamodel --- pms_api_rest/datamodels/pms_amenity_type.py | 1 - pms_api_rest/services/pms_amenity_type_service.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/pms_api_rest/datamodels/pms_amenity_type.py b/pms_api_rest/datamodels/pms_amenity_type.py index dacbf95503..1de011612f 100644 --- a/pms_api_rest/datamodels/pms_amenity_type.py +++ b/pms_api_rest/datamodels/pms_amenity_type.py @@ -14,4 +14,3 @@ class PmsAmenityTypeInfo(Datamodel): _name = "pms.amenity.type.info" id = fields.Integer(required=True, allow_none=False) name = fields.String(required=True, allow_none=False) - pmsAmenityIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/services/pms_amenity_type_service.py b/pms_api_rest/services/pms_amenity_type_service.py index 999d4b5b5d..18a4323c95 100644 --- a/pms_api_rest/services/pms_amenity_type_service.py +++ b/pms_api_rest/services/pms_amenity_type_service.py @@ -54,7 +54,6 @@ def get_amenity_types(self, amenity_types_search_param): PmsAmenityTypeInfo( id=amenity_type.id, name=amenity_type.name, - pmsAmenityIds=amenity_type.pms_amenity_ids.ids, ) ) return result_amenity_types @@ -80,7 +79,6 @@ def get_amenity_type(self, amenity_type_id): return PmsAmenityTypeInfo( id=amenity_type.id, name=amenity_type.name, - pmsAmenityIds=amenity_type.pms_amenity_ids.ids, ) else: raise MissingError(_("Amenity Type not found")) From e40d971d5013b6e06a9808d3c1d6b80f3fb4ba96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 22 Jun 2022 10:13:24 +0200 Subject: [PATCH 108/547] [ADD]pms_api_rest: boards services --- pms_api_rest/datamodels/__init__.py | 3 + pms_api_rest/datamodels/pms_board_service.py | 19 ++++ .../datamodels/pms_board_service_line.py | 17 ++++ pms_api_rest/services/__init__.py | 3 + .../pms_board_service_line_service.py | 81 +++++++++++++++++ .../services/pms_board_service_service.py | 91 +++++++++++++++++++ 6 files changed, 214 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_board_service.py create mode 100644 pms_api_rest/datamodels/pms_board_service_line.py create mode 100644 pms_api_rest/services/pms_board_service_line_service.py create mode 100644 pms_api_rest/services/pms_board_service_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 199a2eab6b..35f81da72d 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -34,3 +34,6 @@ from . import pms_amenity_type from . import pms_amenity + +from . import pms_board_service +from . import pms_board_service_line 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..ebf27b4b3c --- /dev/null +++ b/pms_api_rest/datamodels/pms_board_service.py @@ -0,0 +1,19 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsBoardServiceSearchParam(Datamodel): + _name = "pms.board.service.search.param" + ids = fields.List(fields.Integer(required=False, allow_none=True)) + 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) 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..1820f8a239 --- /dev/null +++ b/pms_api_rest/datamodels/pms_board_service_line.py @@ -0,0 +1,17 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsBoardServiceLineSearchParam(Datamodel): + _name = "pms.board.service.line.search.param" + pmsBoarServiceId = 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) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index fc97b6bb8b..a4ed644127 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -20,3 +20,6 @@ from . import pms_amenity_service from . import pms_amenity_type_service + +from . import pms_board_service_line_service +from . import pms_board_service_service 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..7ae8cc8413 --- /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-line" + _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.pmsBoardServiceId: + domain.append( + ( + "pms_board_service_room_type_id", + "=", + board_service_lines_search_param.pmsBoardServiceId, + ) + ) + + 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=line.amount, + ) + ) + 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=board_service_line.amount, + ) + 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..4462077c21 --- /dev/null +++ b/pms_api_rest/services/pms_board_service_service.py @@ -0,0 +1,91 @@ +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-service" + _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): + domain = [] + if board_services_search_param.name: + domain.append(("name", "like", board_services_search_param.name)) + if board_services_search_param.ids: + domain.append(("id", "in", board_services_search_param.ids)) + 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_ids", + "in", + board_services_search_param.pmsPropertyId, + ), + ("pms_property_ids", "=", False), + ] + ) + + 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=board_service.amount, + ) + ) + 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=board_service.amount, + ) + else: + raise MissingError(_("Amenity Type not found")) From 34327a317c4979da145425014942ea9a0a379375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 22 Jun 2022 11:28:01 +0200 Subject: [PATCH 109/547] [ADD]pms_api_rest: overbooking reservation service --- pms_api_rest/datamodels/pms_reservation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index f002fdf160..d74a2f4c79 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -26,3 +26,4 @@ class PmsReservationInfo(Datamodel): boardServiceName = fields.String(required=False, allow_none=True) channelTypeId = fields.Integer(required=False, allow_none=True) adults = fields.Integer(required=False, allow_none=True) + Overbooking = fields.Boolean(required=False, allow_none=True) From 18085b963de9e427279f1238bd0f9e0126a7e592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 22 Jun 2022 17:08:54 +0200 Subject: [PATCH 110/547] [ADD]pms_api_rest: product services --- pms_api_rest/datamodels/__init__.py | 2 + pms_api_rest/datamodels/pms_product.py | 23 ++++ pms_api_rest/services/__init__.py | 2 + .../services/pms_board_service_service.py | 44 +++++++ pms_api_rest/services/pms_product_service.py | 117 ++++++++++++++++++ 5 files changed, 188 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_product.py create mode 100644 pms_api_rest/services/pms_product_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 35f81da72d..b0bec9988c 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -37,3 +37,5 @@ from . import pms_board_service from . import pms_board_service_line + +from . import pms_product diff --git a/pms_api_rest/datamodels/pms_product.py b/pms_api_rest/datamodels/pms_product.py new file mode 100644 index 0000000000..7ceabe03d0 --- /dev/null +++ b/pms_api_rest/datamodels/pms_product.py @@ -0,0 +1,23 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsProductSearchParam(Datamodel): + _name = "pms.product.search.param" + ids = fields.List(fields.Integer(required=False, allow_none=True)) + name = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=True, allow_none=False) + pricelistId = fields.Integer(required=True, allow_none=False) + partnerId = fields.Integer(required=False, allow_none=True) + dateConsumption = fields.String(required=False, allow_none=True) + productQty = fields.Integer(required=False, allow_none=True) + + +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) + price = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index a4ed644127..f9817f835e 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -23,3 +23,5 @@ from . import pms_board_service_line_service from . import pms_board_service_service + +from . import pms_product_service diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index 4462077c21..06312dda9b 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -89,3 +89,47 @@ def get_board_service(self, board_service_id): ) else: raise MissingError(_("Amenity Type 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.pms_property_id: + domain.extend( + [ + "|", + ( + "pms_property_ids", + "in", + pms_search_param.pms_property_id, + ), + ("pms_property_ids", "=", False), + ] + ) + 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=line.amount, + ) + ) + return result_board_service_lines 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..d4ee084898 --- /dev/null +++ b/pms_api_rest/services/pms_product_service.py @@ -0,0 +1,117 @@ +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 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)] + if product_search_param.name: + domain.append(("name", "like", product_search_param.name)) + if product_search_param.ids: + domain.append(("id", "in", product_search_param.ids)) + 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, + price=self._get_product_price(product, product_search_param), + perDay=product.per_day, + perPerson=product.per_person, + ) + ) + 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, + price=self._get_product_price(product, product_search_param), + perDay=product.per_day, + perPerson=product.per_person, + ) + else: + raise MissingError(_("Product not found")) + + def _get_product_price(self, product, product_search_param): + pms_property = self.env["pms.property"].browse( + product_search_param.pmsPropertyId + ) + product_context = dict( + self.env.context, + date=datetime.today().date(), + pricelist=product_search_param.pricelistId or False, + uom=product.uom_id.id, + fiscal_position=False, + property=product_search_param.pmsPropertyId, + ) + if product_search_param.dateConsumption: + product_context["consumption_date"] = product_search_param.dateConsumption + 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=product_search_param.pricelistId, + product=product, + company_id=pms_property.company_id.id, + product_qty=product_search_param.productQty or 1, + partner_id=product_search_param.partnerId, + ), + product.taxes_id, + product.taxes_id, # Not exist service line, we repeat product taxes + pms_property.company_id, + ) From 140c2c36d84d4e3ff8a9c6745735883a05eef0e9 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 6 Jul 2022 11:14:23 +0200 Subject: [PATCH 111/547] [WIP] pms-api-rest: pending update reservation by id --- pms_api_rest/datamodels/pms_amenity.py | 3 +-- .../datamodels/pms_reservation_line.py | 14 +++++----- pms_api_rest/datamodels/pms_room.py | 10 ++++--- pms_api_rest/services/pms_amenity_service.py | 5 ++-- pms_api_rest/services/pms_folio_service.py | 6 +++-- .../services/pms_reservation_service.py | 21 +++++++++++++++ pms_api_rest/services/pms_room_service.py | 27 +++++++++++++++++++ 7 files changed, 70 insertions(+), 16 deletions(-) diff --git a/pms_api_rest/datamodels/pms_amenity.py b/pms_api_rest/datamodels/pms_amenity.py index f8bfe33968..c95f244fe4 100644 --- a/pms_api_rest/datamodels/pms_amenity.py +++ b/pms_api_rest/datamodels/pms_amenity.py @@ -14,5 +14,4 @@ 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) - pmsAmenityTypeId = fields.Integer(required=True, allow_none=False) + amenityTypeId = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_reservation_line.py b/pms_api_rest/datamodels/pms_reservation_line.py index 22f770e01c..f92d02ea6e 100644 --- a/pms_api_rest/datamodels/pms_reservation_line.py +++ b/pms_api_rest/datamodels/pms_reservation_line.py @@ -5,10 +5,12 @@ class PmsReservationLineSearchParam(Datamodel): _name = "pms.reservation.line.search.param" - id = fields.Integer(required=False, allow_none=True) - date = fields.String(required=False, allow_none=True) - reservationId = fields.Integer(required=False, allow_none=True) - pmsPropertyId = fields.Integer(required=False, allow_none=True) + id = fields.Integer(required=False, allow_none=False) + date = 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): @@ -16,8 +18,8 @@ class PmsReservationLineInfo(Datamodel): id = fields.Integer(required=False, allow_none=False) date = fields.String(required=False, allow_none=False) price = fields.Float(required=False, allow_none=False) - discount = fields.Float(required=False, allow_none=True) - cancelDiscount = fields.Float(required=False, allow_none=True) + discount = fields.Float(required=False, allow_none=False) + cancelDiscount = fields.Float(required=False, allow_none=False) roomId = fields.Integer(required=False, allow_none=False) reservationId = fields.Integer(required=False, allow_none=False) pmsPropertyId = fields.Integer(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index fffc82ca5b..c44ab28904 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -5,9 +5,13 @@ class PmsRoomSearchParam(Datamodel): _name = "pms.room.search.param" - id = fields.Integer(required=False, allow_none=True) - name = fields.String(required=False, allow_none=True) - pms_property_id = fields.Integer(required=False, allow_none=True) + id = fields.Integer(required=False, allow_none=False) + name = fields.String(required=False, allow_none=False) + pms_property_id = 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): diff --git a/pms_api_rest/services/pms_amenity_service.py b/pms_api_rest/services/pms_amenity_service.py index edca83c729..a722c3b945 100644 --- a/pms_api_rest/services/pms_amenity_service.py +++ b/pms_api_rest/services/pms_amenity_service.py @@ -26,7 +26,7 @@ class PmsAmenityService(Component): auth="jwt_api_pms", ) def get_amenities(self, amenities_search_param): - domain = [] + domain = [("pms_amenity_type_id", "!=", False)] if amenities_search_param.name: domain.append(("name", "like", amenities_search_param.name)) if amenities_search_param.id: @@ -50,8 +50,7 @@ def get_amenities(self, amenities_search_param): PmsAmenityInfo( id=amenity.id, name=amenity.name, - defaultCode=amenity.default_code, - pmsAmenityTypeId=amenity.pms_amenity_type_id.id, + amenityTypeId=amenity.pms_amenity_type_id.id, ) ) return result_amenities diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 6742a4b3c7..45730a8e61 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -82,7 +82,9 @@ def get_folios(self, folio_search_param): reservation_lines.append( { "id": reservation_line.id, - "date": reservation_line.date, + "date": datetime.combine( + reservation_line.date, datetime.min.time() + ).isoformat(), "roomId": reservation_line.room_id.id, "roomName": reservation_line.room_id.name, } @@ -103,7 +105,7 @@ def get_folios(self, folio_search_param): "checkout": datetime.combine( reservation.checkout, datetime.min.time() ).isoformat(), - "preferredRoomId": reservation.preferred_room_id.name + "preferredRoomId": reservation.preferred_room_id.id if reservation.preferred_room_id else "", "preferredRoomCapacity": reservation.preferred_room_id.capacity diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 8d65f4cbbe..c557f4dae0 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -208,6 +208,27 @@ def update_reservation(self, reservation_id, reservation_lines_changes): ) reservation_to_update.write(reservation_vals) + @restapi.method( + [ + ( + [ + "//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 + @restapi.method( [ ( diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index c1dadb5b7d..c05dd36d27 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -1,3 +1,5 @@ +from datetime import datetime + from odoo import _ from odoo.exceptions import MissingError @@ -33,6 +35,30 @@ def get_rooms(self, room_search_param): domain.append(("id", "=", room_search_param.id)) if room_search_param.pms_property_id: domain.append(("pms_property_id", "=", room_search_param.pms_property_id)) + if ( + room_search_param.availabilityFrom + and room_search_param.availabilityTo + and room_search_param.pms_property_id + and room_search_param.pricelistId + ): + 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.pms_property_id + ) + 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"] @@ -54,6 +80,7 @@ def get_rooms(self, room_search_param): roomTypeClassId=room.room_type_id.class_id, ubicationId=room.ubication_id, extraBedsAllowed=room.extra_beds_allowed, + roomAmenityIds=room.room_amenity_ids.ids, ) ) return result_rooms From ece734b84d8ef6c7c7f81e82baee74fdfeef0384 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 6 Jul 2022 20:11:25 +0200 Subject: [PATCH 112/547] [REF]pms_api_rest: api rest routes and datamodel names refactor --- .../datamodels/pms_account_journal.py | 2 +- .../datamodels/pms_availability_plan.py | 2 +- .../datamodels/pms_availability_plan_rule.py | 6 +-- pms_api_rest/datamodels/pms_calendar.py | 10 ++--- pms_api_rest/datamodels/pms_folio.py | 6 +-- pms_api_rest/datamodels/pms_pricelist.py | 2 +- pms_api_rest/datamodels/pms_pricelist_item.py | 6 +-- pms_api_rest/datamodels/pms_reservation.py | 4 +- pms_api_rest/datamodels/pms_room.py | 2 +- pms_api_rest/datamodels/pms_room_type.py | 4 +- .../datamodels/pms_room_type_class.py | 4 +- pms_api_rest/datamodels/pms_search_param.py | 4 +- pms_api_rest/datamodels/pms_ubication.py | 4 +- .../services/pms_availability_plan_service.py | 12 +++--- .../services/pms_board_service_service.py | 4 +- pms_api_rest/services/pms_calendar_service.py | 40 +++++++++---------- pms_api_rest/services/pms_folio_service.py | 14 +++---- .../services/pms_pricelist_service.py | 12 +++--- pms_api_rest/services/pms_property_service.py | 4 +- .../services/pms_reservation_service.py | 12 +++--- pms_api_rest/services/pms_room_service.py | 8 ++-- .../services/pms_room_type_class_service.py | 6 +-- .../services/pms_room_type_services.py | 6 +-- .../services/pms_ubication_service.py | 6 +-- pms_api_rest/services/res_country_services.py | 2 +- 25 files changed, 91 insertions(+), 91 deletions(-) diff --git a/pms_api_rest/datamodels/pms_account_journal.py b/pms_api_rest/datamodels/pms_account_journal.py index 25e2f76833..48527e3464 100644 --- a/pms_api_rest/datamodels/pms_account_journal.py +++ b/pms_api_rest/datamodels/pms_account_journal.py @@ -7,4 +7,4 @@ class PmsAccountJournalInfo(Datamodel): _name = "pms.account.journal.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - allowed_pms_payments = fields.Boolean(required=False, allow_none=True) + allowedPmsPayments = fields.Boolean(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 index 9e5cd2fb07..c0efee5fdd 100644 --- a/pms_api_rest/datamodels/pms_availability_plan.py +++ b/pms_api_rest/datamodels/pms_availability_plan.py @@ -7,4 +7,4 @@ class PmsAvailabilityPlanInfo(Datamodel): _name = "pms.availability.plan.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - pms_property_ids = fields.List(fields.Integer(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 index 6e600a4c34..e25373f921 100644 --- a/pms_api_rest/datamodels/pms_availability_plan_rule.py +++ b/pms_api_rest/datamodels/pms_availability_plan_rule.py @@ -5,9 +5,9 @@ class PmsAvailabilityPlanRuleSearchParam(Datamodel): _name = "pms.availability.plan.rule.search.param" - date_from = fields.String(required=True, allow_none=False) - date_to = fields.String(required=True, allow_none=False) - pms_property_id = fields.Integer(required=True, allow_none=False) + 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 PmsAvailabilityPlanRuleInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 9391864e7e..e840d639c4 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -20,15 +20,15 @@ class PmsCalendarSwapInfo(Datamodel): swapTo = fields.String(required=True, allow_none=False) roomIdA = fields.Integer(required=True, allow_none=False) roomIdB = fields.Integer(required=True, allow_none=False) - pms_property_id = fields.Integer(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) class PmsCalendarSearchParam(Datamodel): _name = "pms.calendar.search.param" - date_from = fields.String(required=False, allow_none=True) - date_to = fields.String(required=False, allow_none=True) - pms_property_id = fields.Integer(required=True, allow_none=False) - pricelist_id = fields.Integer(required=False, allow_none=True) + 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) class PmsCalendarFreeDailyRoomsByType(Datamodel): diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 5c8360ee0c..be173c5584 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -6,9 +6,9 @@ class PmsFolioSearchParam(Datamodel): _name = "pms.folio.search.param" - pms_property_id = fields.Integer(required=True, allow_none=True) - date_from = fields.String(required=False, allow_none=True) - date_to = fields.String(required=False, allow_none=True) + 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) diff --git a/pms_api_rest/datamodels/pms_pricelist.py b/pms_api_rest/datamodels/pms_pricelist.py index 0b67c86b56..cf10e57fa7 100644 --- a/pms_api_rest/datamodels/pms_pricelist.py +++ b/pms_api_rest/datamodels/pms_pricelist.py @@ -7,4 +7,4 @@ class PmsPricelistInfo(Datamodel): _name = "pms.pricelist.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - pms_property_ids = fields.List(fields.Integer(required=False, allow_none=True)) + pmsPropertyIds = 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 index 988ad691dd..59df5b8867 100644 --- a/pms_api_rest/datamodels/pms_pricelist_item.py +++ b/pms_api_rest/datamodels/pms_pricelist_item.py @@ -5,9 +5,9 @@ class PmsPricelistItemSearchParam(Datamodel): _name = "pms.pricelist.item.search.param" - date_from = fields.String(required=True, allow_none=False) - date_to = fields.String(required=True, allow_none=False) - pms_property_id = fields.Integer(required=True, allow_none=False) + 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): diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index d74a2f4c79..098daf57c5 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -21,9 +21,9 @@ class PmsReservationInfo(Datamodel): pricelistId = fields.Integer(required=False, allow_none=True) services = fields.List(fields.Dict(required=False, allow_none=True)) messages = fields.List(fields.Dict(required=False, allow_none=True)) - pms_property_id = fields.Integer(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) boardServiceId = fields.Integer(required=False, allow_none=True) boardServiceName = fields.String(required=False, allow_none=True) channelTypeId = fields.Integer(required=False, allow_none=True) adults = fields.Integer(required=False, allow_none=True) - Overbooking = fields.Boolean(required=False, allow_none=True) + overbooking = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index c44ab28904..987d9e770f 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -7,7 +7,7 @@ class PmsRoomSearchParam(Datamodel): _name = "pms.room.search.param" id = fields.Integer(required=False, allow_none=False) name = fields.String(required=False, allow_none=False) - pms_property_id = fields.Integer(required=True, 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) diff --git a/pms_api_rest/datamodels/pms_room_type.py b/pms_api_rest/datamodels/pms_room_type.py index eb4045ad16..9c3645c22f 100644 --- a/pms_api_rest/datamodels/pms_room_type.py +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -7,13 +7,13 @@ class PmsRoomTypeSearchParam(Datamodel): _name = "pms.room.type.search.param" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - pms_property_ids = fields.List(fields.Integer(), required=False) + 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) - pms_property_ids = fields.List(fields.Integer(), required=False) + pmsPropertyIds = fields.List(fields.Integer(), required=False) defaultCode = fields.String(required=False, allow_none=True) price = fields.Float(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 index dbc60131de..b358661738 100644 --- a/pms_api_rest/datamodels/pms_room_type_class.py +++ b/pms_api_rest/datamodels/pms_room_type_class.py @@ -7,11 +7,11 @@ class PmsRoomTypeClassSearchParam(Datamodel): _name = "pms.room.type.class.search.param" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - pms_property_ids = fields.List(fields.Integer(), required=False) + 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) - pms_property_ids = fields.List(fields.Integer(), required=False) + pmsPropertyIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_search_param.py b/pms_api_rest/datamodels/pms_search_param.py index f71a4fe2dc..d7951a104f 100644 --- a/pms_api_rest/datamodels/pms_search_param.py +++ b/pms_api_rest/datamodels/pms_search_param.py @@ -6,5 +6,5 @@ class PmsSearchParam(Datamodel): _name = "pms.search.param" - pms_property_id = fields.Integer(required=False, allow_none=True) - pms_property_ids = fields.List(fields.Integer(), required=False) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_ubication.py b/pms_api_rest/datamodels/pms_ubication.py index ad7ae70591..8e7a601181 100644 --- a/pms_api_rest/datamodels/pms_ubication.py +++ b/pms_api_rest/datamodels/pms_ubication.py @@ -7,11 +7,11 @@ class PmsUbicationSearchParam(Datamodel): _name = "pms.ubication.search.param" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - pms_property_ids = fields.List(fields.Integer(), required=False) + 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) - pms_property_ids = fields.List(fields.Integer(), required=False) + pmsPropertyIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index c878b078c7..e51a64043b 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -32,8 +32,8 @@ def get_availability_plans(self, pms_search_param, **args): [("pms_property_ids", "=", False)] ) availabilities = set() - if pms_search_param.pms_property_ids: - for index, prop in enumerate(pms_search_param.pms_property_ids): + 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)]) @@ -59,7 +59,7 @@ def get_availability_plans(self, pms_search_param, **args): PmsAvialabilityPlanInfo( id=availability.id, name=availability.name, - pms_property_ids=availability.pms_property_ids.mapped("id"), + pmsPropertyIds=availability.pms_property_ids.mapped("id"), ) ) return result_availabilities @@ -92,15 +92,15 @@ def get_availability_plan_rules( ( "pms_property_id", "=", - availability_plan_rule_search_param.pms_property_id, + availability_plan_rule_search_param.pmsPropertyId, ) ] ) date_from = datetime.strptime( - availability_plan_rule_search_param.date_from, "%Y-%m-%d" + availability_plan_rule_search_param.dateFrom, "%Y-%m-%d" ).date() date_to = datetime.strptime( - availability_plan_rule_search_param.date_to, "%Y-%m-%d" + availability_plan_rule_search_param.dateTo, "%Y-%m-%d" ).date() for date in ( diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index 06312dda9b..51a4e57307 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -106,14 +106,14 @@ def get_board_service(self, board_service_id): 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.pms_property_id: + if pms_search_param.pmsPropertyId: domain.extend( [ "|", ( "pms_property_ids", "in", - pms_search_param.pms_property_id, + pms_search_param.pmsPropertyId, ), ("pms_property_ids", "=", False), ] diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 5dcf796694..2b44deeecc 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -26,9 +26,9 @@ class PmsCalendarService(Component): ) def get_calendar(self, calendar_search_param): domain = list() - domain.append(("date", ">=", calendar_search_param.date_from)) - domain.append(("date", "<=", calendar_search_param.date_to)) - domain.append(("pms_property_id", "=", calendar_search_param.pms_property_id)) + domain.append(("date", ">=", calendar_search_param.dateFrom)) + domain.append(("date", "<=", calendar_search_param.dateTo)) + domain.append(("pms_property_id", "=", calendar_search_param.pmsPropertyId)) domain.append(("state", "!=", "cancel")) result_lines = [] PmsCalendarInfo = self.env.datamodels["pms.calendar.info"] @@ -117,7 +117,7 @@ def swap_reservation_slices(self, swap_info): ("room_id", "=", room_id_b), ("date", ">=", swap_info.swapFrom), ("date", "<=", swap_info.swapTo), - ("pms_property_id", "=", swap_info.pms_property_id), + ("pms_property_id", "=", swap_info.pmsPropertyId), ] ) lines_room_a.occupies_availability = False @@ -146,24 +146,24 @@ def swap_reservation_slices(self, swap_info): def get_daily_invoincing(self, pms_calendar_search_param): reservation_lines = self.env["pms.reservation.line"].search( [ - ("date", ">=", pms_calendar_search_param.date_from), - ("date", "<=", pms_calendar_search_param.date_to), - ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), + ("date", ">=", pms_calendar_search_param.dateFrom), + ("date", "<=", pms_calendar_search_param.dateTo), + ("pms_property_id", "=", pms_calendar_search_param.pmsPropertyId), ] ) service_lines = self.env["pms.service.line"].search( [ - ("date", ">=", pms_calendar_search_param.date_from), - ("date", "<=", pms_calendar_search_param.date_to), - ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), + ("date", ">=", pms_calendar_search_param.dateFrom), + ("date", "<=", pms_calendar_search_param.dateTo), + ("pms_property_id", "=", pms_calendar_search_param.pmsPropertyId), ] ) date_from = datetime.strptime( - pms_calendar_search_param.date_from, "%Y-%m-%d" + pms_calendar_search_param.dateFrom, "%Y-%m-%d" ).date() date_to = datetime.strptime( - pms_calendar_search_param.date_to, "%Y-%m-%d" + pms_calendar_search_param.dateTo, "%Y-%m-%d" ).date() result = [] @@ -201,10 +201,10 @@ def get_daily_invoincing(self, pms_calendar_search_param): def get_free_rooms(self, pms_calendar_search_param): date_from = datetime.strptime( - pms_calendar_search_param.date_from, "%Y-%m-%d" + pms_calendar_search_param.dateFrom, "%Y-%m-%d" ).date() date_to = datetime.strptime( - pms_calendar_search_param.date_to, "%Y-%m-%d" + pms_calendar_search_param.dateTo, "%Y-%m-%d" ).date() result = [] PmsCalendarFreeDailyRoomsByType = self.env.datamodels[ @@ -214,7 +214,7 @@ def get_free_rooms(self, pms_calendar_search_param): date_from + timedelta(d) for d in range((date_to - date_from).days + 1) ): rooms = self.env["pms.room"].search( - [("pms_property_id", "=", pms_calendar_search_param.pms_property_id)] + [("pms_property_id", "=", pms_calendar_search_param.pmsPropertyId)] ) for room_type_iterator in self.env["pms.room.type"].search( [("id", "in", rooms.mapped("room_type_id").ids)] @@ -227,7 +227,7 @@ def get_free_rooms(self, pms_calendar_search_param): ( "pms_property_id", "=", - pms_calendar_search_param.pms_property_id, + pms_calendar_search_param.pmsPropertyId, ), ] ) @@ -236,7 +236,7 @@ def get_free_rooms(self, pms_calendar_search_param): ( "pms_property_id", "=", - pms_calendar_search_param.pms_property_id, + pms_calendar_search_param.pmsPropertyId, ), ("room_type_id", "=", room_type_iterator.id), ] @@ -274,10 +274,10 @@ def get_free_rooms(self, pms_calendar_search_param): def get_alerts_per_day(self, pms_calendar_search_param): PmsCalendarAlertsPerDay = self.env.datamodels["pms.calendar.alerts.per.day"] date_from = datetime.strptime( - pms_calendar_search_param.date_from, "%Y-%m-%d" + pms_calendar_search_param.dateFrom, "%Y-%m-%d" ).date() date_to = datetime.strptime( - pms_calendar_search_param.date_to, "%Y-%m-%d" + pms_calendar_search_param.dateTo, "%Y-%m-%d" ).date() result = [] for day in ( @@ -286,7 +286,7 @@ def get_alerts_per_day(self, pms_calendar_search_param): lines = self.env["pms.reservation.line"].search_count( [ ("date", "=", day), - ("pms_property_id", "=", pms_calendar_search_param.pms_property_id), + ("pms_property_id", "=", pms_calendar_search_param.pmsPropertyId), ("overbooking", "=", True), ] ) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 45730a8e61..ed6d27d709 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -30,16 +30,16 @@ def get_folios(self, folio_search_param): domain_fields = list() domain_fields.append( - ("pms_property_id", "=", folio_search_param.pms_property_id) + ("pms_property_id", "=", folio_search_param.pmsPropertyId) ) - if folio_search_param.date_to and folio_search_param.date_from: + if folio_search_param.dateTo and folio_search_param.dateFrom: reservation_lines = ( self.env["pms.reservation.line"] .search( [ - ("date", ">=", folio_search_param.date_from), - ("date", "<", folio_search_param.date_to), + ("date", ">=", folio_search_param.dateFrom), + ("date", "<", folio_search_param.dateTo), ] ) .mapped("reservation_id") @@ -107,7 +107,7 @@ def get_folios(self, folio_search_param): ).isoformat(), "preferredRoomId": reservation.preferred_room_id.id if reservation.preferred_room_id - else "", + else 0, "preferredRoomCapacity": reservation.preferred_room_id.capacity if reservation.preferred_room_id else "", @@ -219,7 +219,7 @@ def get_folios(self, folio_search_param): def get_folio_payments(self, folio_id, pms_search_param): domain = list() domain.append(("id", "=", folio_id)) - domain.append(("pms_property_id", "=", pms_search_param.pms_property_id)) + domain.append(("pms_property_id", "=", pms_search_param.pmsPropertyId)) folio = self.env["pms.folio"].search(domain) payments = [] PmsPaymentInfo = self.env.datamodels["pms.payment.info"] @@ -271,7 +271,7 @@ def create_reservation(self, pms_reservation_info): reservation = self.env["pms.reservation"].create( { "partner_name": pms_reservation_info.partner, - "pms_property_id": pms_reservation_info.pms_property_id, + "pms_property_id": pms_reservation_info.pmsPropertyId, "room_type_id": pms_reservation_info.roomTypeId, "pricelist_id": pms_reservation_info.pricelistId, "checkin": pms_reservation_info.checkin, diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 039d739c55..6949f0e0ed 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -32,9 +32,9 @@ def get_pricelists(self, pms_search_param, **args): pricelists_all_properties = self.env["product.pricelist"].search( [("pms_property_ids", "=", False)] ) - if pms_search_param.pms_property_ids: + if pms_search_param.pmsPropertyIds: pricelists = set() - for index, prop in enumerate(pms_search_param.pms_property_ids): + for index, prop in enumerate(pms_search_param.pmsPropertyIds): pricelists_with_query_property = self.env["product.pricelist"].search( [("pms_property_ids", "=", prop)] ) @@ -60,7 +60,7 @@ def get_pricelists(self, pms_search_param, **args): PmsPricelistInfo( id=pricelist.id, name=pricelist.name, - pms_property_ids=pricelist.pms_property_ids.mapped("id"), + pmsPropertyIds=pricelist.pms_property_ids.mapped("id"), ) ) return result_pricelists @@ -87,13 +87,13 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): raise MissingError PmsPricelistItemInfo = self.env.datamodels["pms.pricelist.item.info"] rooms = self.env["pms.room"].search( - [("pms_property_id", "=", pricelist_item_search_param.pms_property_id)] + [("pms_property_id", "=", pricelist_item_search_param.pmsPropertyId)] ) date_from = datetime.strptime( - pricelist_item_search_param.date_from, "%Y-%m-%d" + pricelist_item_search_param.dateFrom, "%Y-%m-%d" ).date() date_to = datetime.strptime( - pricelist_item_search_param.date_to, "%Y-%m-%d" + pricelist_item_search_param.dateTo, "%Y-%m-%d" ).date() for date in ( diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 542b935c56..991eaaed08 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -96,7 +96,7 @@ def get_property(self, property_id): [ ( [ - "//paymentmethods", + "//payment-methods", ], "GET", ) @@ -120,7 +120,7 @@ def get_method_payments_property(self, property_id): PmsAccountJournalInfo( id=payment_method.id, name=payment_method.name, - allowed_pms_payments=payment_method.allowed_pms_payments, + allowedPmsPayments=payment_method.allowed_pms_payments, ) ) return res diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index c557f4dae0..c09954a7f4 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -27,8 +27,8 @@ class PmsReservationService(Component): def get_reservation(self, reservation_id, pms_search_param): domain = list() domain.append(("id", "=", reservation_id)) - if pms_search_param.pms_property_id: - domain.append(("pms_property_id", "=", pms_search_param.pms_property_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"] @@ -233,7 +233,7 @@ def update_reservation_lines( [ ( [ - "//checkinpartners", + "//checkin-partners", ], "GET", ) @@ -314,7 +314,7 @@ def get_checkin_partners(self, reservation_id): [ ( [ - "//checkin_partners/", + "//checkin-partners/", ], "PATCH", ) @@ -337,7 +337,7 @@ def write_reservation_checkin_partner( [ ( [ - "//checkin_partners", + "//checkin-partners", ], "POST", ) @@ -368,7 +368,7 @@ def create_reservation_checkin_partner( [ ( [ - "//checkin_partners/", + "//checkin-partners/", ], "DELETE", ) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index c05dd36d27..6d5e200c71 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -33,12 +33,12 @@ def get_rooms(self, room_search_param): domain.append(("name", "like", room_search_param.name)) if room_search_param.id: domain.append(("id", "=", room_search_param.id)) - if room_search_param.pms_property_id: - domain.append(("pms_property_id", "=", room_search_param.pms_property_id)) + 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.pms_property_id + and room_search_param.pmsPropertyId and room_search_param.pricelistId ): date_from = datetime.strptime( @@ -48,7 +48,7 @@ def get_rooms(self, room_search_param): room_search_param.availabilityTo, "%Y-%m-%d" ).date() pms_property = self.env["pms.property"].browse( - room_search_param.pms_property_id + room_search_param.pmsPropertyId ) pms_property = pms_property.with_context( checkin=date_from, diff --git a/pms_api_rest/services/pms_room_type_class_service.py b/pms_api_rest/services/pms_room_type_class_service.py index 1d35f7c9b9..f767b65826 100644 --- a/pms_api_rest/services/pms_room_type_class_service.py +++ b/pms_api_rest/services/pms_room_type_class_service.py @@ -26,9 +26,9 @@ def get_room_type_class(self, room_type_class_search_param): room_type_class_all_properties = self.env["pms.room.type.class"].search( [("pms_property_ids", "=", False)] ) - if room_type_class_search_param.pms_property_ids: + if room_type_class_search_param.pmsPropertyIds: room_type_class = set() - for index, prop in enumerate(room_type_class_search_param.pms_property_ids): + for index, prop in enumerate(room_type_class_search_param.pmsPropertyds): room_type_class_with_query_property = self.env[ "pms.room.type.class" ].search([("pms_property_ids", "=", prop)]) @@ -57,7 +57,7 @@ def get_room_type_class(self, room_type_class_search_param): PmsRoomTypeClassInfo( id=room.id, name=room.name, - pms_property_ids=room.pms_property_ids.mapped("id"), + pmsPropertyIds=room.pms_property_ids.mapped("id"), ) ) return result_room_type_class diff --git a/pms_api_rest/services/pms_room_type_services.py b/pms_api_rest/services/pms_room_type_services.py index 448d747891..8fef9c3608 100644 --- a/pms_api_rest/services/pms_room_type_services.py +++ b/pms_api_rest/services/pms_room_type_services.py @@ -26,9 +26,9 @@ 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.pms_property_ids: + if room_type_search_param.pmsPropertyIds: room_types = set() - for index, prop in enumerate(room_type_search_param.pms_property_ids): + 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)] ) @@ -57,7 +57,7 @@ def get_room_types(self, room_type_search_param): PmsRoomTypeInfo( id=room.id, name=room.name, - pms_property_ids=room.pms_property_ids.mapped("id"), + pmsPropertyIds=room.pms_property_ids.mapped("id"), defaultCode=room.default_code, price=room.list_price, ) diff --git a/pms_api_rest/services/pms_ubication_service.py b/pms_api_rest/services/pms_ubication_service.py index 1f03bd5bc5..a2fe085114 100644 --- a/pms_api_rest/services/pms_ubication_service.py +++ b/pms_api_rest/services/pms_ubication_service.py @@ -26,9 +26,9 @@ def get_ubications(self, ubication_search_param): ubication_all_properties = self.env["pms.ubication"].search( [("pms_property_ids", "=", False)] ) - if ubication_search_param.pms_property_ids: + if ubication_search_param.pmsPropertyIds: ubication = set() - for index, prop in enumerate(ubication_search_param.pms_property_ids): + for index, prop in enumerate(ubication_search_param.pmsPropertyIds): ubication_with_query_property = self.env["pms.ubication"].search( [("pms_property_ids", "=", prop)] ) @@ -55,7 +55,7 @@ def get_ubications(self, ubication_search_param): PmsUbicationInfo( id=room.id, name=room.name, - pms_property_ids=room.pms_property_ids.mapped("id"), + pmsPropertyIds=room.pms_property_ids.mapped("id"), ) ) return result_ubications diff --git a/pms_api_rest/services/res_country_services.py b/pms_api_rest/services/res_country_services.py index 6236bc9c86..e5ff5e0e93 100644 --- a/pms_api_rest/services/res_country_services.py +++ b/pms_api_rest/services/res_country_services.py @@ -37,7 +37,7 @@ def get_countries(self): [ ( [ - "//country_states", + "//country-states", ], "GET", ) From 6349c2dc010d53f106e9f3d6ad17ef4d56d966c7 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 7 Jul 2022 09:58:18 +0200 Subject: [PATCH 113/547] [REF]pms_api_rest: rename fields from services to service --- pms_api_rest/services/__init__.py | 6 +++--- .../{pms_room_type_services.py => pms_room_type_service.py} | 0 .../{res_country_services.py => res_country_service.py} | 0 ...category_services.py => res_partner_category_service.py} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename pms_api_rest/services/{pms_room_type_services.py => pms_room_type_service.py} (100%) rename pms_api_rest/services/{res_country_services.py => res_country_service.py} (100%) rename pms_api_rest/services/{res_partner_category_services.py => res_partner_category_service.py} (100%) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index f9817f835e..5fe03c63f0 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -1,6 +1,6 @@ from . import pms_folio_service from . import pms_room_service -from . import pms_room_type_services +from . import pms_room_type_service from . import pms_calendar_service from . import pms_partner_service @@ -11,8 +11,8 @@ from . import pms_pricelist_service from . import pms_availability_plan_service from . import pms_id_categories_service -from . import res_country_services -from . import res_partner_category_services +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 diff --git a/pms_api_rest/services/pms_room_type_services.py b/pms_api_rest/services/pms_room_type_service.py similarity index 100% rename from pms_api_rest/services/pms_room_type_services.py rename to pms_api_rest/services/pms_room_type_service.py diff --git a/pms_api_rest/services/res_country_services.py b/pms_api_rest/services/res_country_service.py similarity index 100% rename from pms_api_rest/services/res_country_services.py rename to pms_api_rest/services/res_country_service.py diff --git a/pms_api_rest/services/res_partner_category_services.py b/pms_api_rest/services/res_partner_category_service.py similarity index 100% rename from pms_api_rest/services/res_partner_category_services.py rename to pms_api_rest/services/res_partner_category_service.py From 463e46f9ecb1fecfe325111f6b98ff75dce50214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 7 Jul 2022 13:10:16 +0200 Subject: [PATCH 114/547] [RFC]pms_api_rest: Replace subdict folio by reservation datamodel and service --- pms_api_rest/datamodels/pms_folio.py | 1 - pms_api_rest/datamodels/pms_reservation.py | 49 ++++-- pms_api_rest/services/pms_folio_service.py | 186 ++++++++++----------- 3 files changed, 121 insertions(+), 115 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index be173c5584..d1de818ac2 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -23,7 +23,6 @@ class PmsFolioInfo(Datamodel): agency = fields.String(required=False, allow_none=True) state = fields.String(required=False, allow_none=True) pendingAmount = fields.Float(required=False, allow_none=True) - reservations = fields.List(fields.Dict(required=False, allow_none=True)) salesPerson = fields.String(required=False, allow_none=True) paymentState = fields.String(required=False, allow_none=True) propertyId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 098daf57c5..d305f62ef8 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -1,3 +1,4 @@ +from sre_parse import State from marshmallow import fields from odoo.addons.datamodel.core import Datamodel @@ -6,24 +7,46 @@ class PmsReservationInfo(Datamodel): _name = "pms.reservation.info" id = fields.Integer(required=False, allow_none=True) - partner = fields.String(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) + partnerName = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) + boardServiceId = fields.Integer(required=False, allow_none=True) + saleChannelId = fields.Integer(required=False, allow_none=True) + agencyId = 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) - roomTypeName = fields.String(required=False, allow_none=True) - preferredRoomName = fields.String(required=False, allow_none=True) preferredRoomId = fields.Integer(required=False, allow_none=True) - priceTotal = fields.Float(required=False, allow_none=True) - priceOnlyServices = fields.Float(required=False, allow_none=True) - priceOnlyRoom = fields.Float(required=False, allow_none=True) - pricelistName = fields.String(required=False, allow_none=True) pricelistId = fields.Integer(required=False, allow_none=True) - services = fields.List(fields.Dict(required=False, allow_none=True)) - messages = fields.List(fields.Dict(required=False, allow_none=True)) - pmsPropertyId = fields.Integer(required=False, allow_none=True) - boardServiceId = fields.Integer(required=False, allow_none=True) - boardServiceName = fields.String(required=False, allow_none=True) - channelTypeId = 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) + state = fields.String(required=False, allow_none=True) + children = fields.Integer(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) + pendingCheckinData = fields.Integer(required=False, allow_none=True) + createDate = fields.String(required=False, allow_none=True) + segmentationId = fields.Integer(required=False, allow_none=True) + cancellationPolicyId = fields.Integer(required=False, allow_none=True) + toAssign = fields.Boolean(required=False, allow_none=True) + reservationType = fields.String(required=False, allow_none=True) + + priceTotal = fields.Float(required=False, allow_none=True) + discount = 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) + pendingAmount = fields.Float(required=False, allow_none=True) + + # TODO: Refact + #services = fields.List(fields.Dict(required=False, allow_none=True)) + #messages = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ed6d27d709..d3d0eae710 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -89,90 +89,9 @@ def get_folios(self, folio_search_param): "roomName": reservation_line.room_id.name, } ) - segmentation_ids = [] - if reservation.segmentation_ids: - for segmentation in reservation.segmentation_ids: - segmentation_ids.append(segmentation.id) - reservations.append( - { - "id": reservation.id, - "name": reservation.name, - "folioSequence": reservation.folio_sequence, - "checkin": datetime.combine( - reservation.checkin, datetime.min.time() - ).isoformat(), - "checkout": datetime.combine( - reservation.checkout, datetime.min.time() - ).isoformat(), - "preferredRoomId": reservation.preferred_room_id.id - if reservation.preferred_room_id - else 0, - "preferredRoomCapacity": reservation.preferred_room_id.capacity - if reservation.preferred_room_id - else "", - "roomTypeName": reservation.room_type_id.name - if reservation.room_type_id - else "", - "roomTypeId": reservation.room_type_id.id - if reservation.room_type_id - else "", - "priceTotal": reservation.price_total, - "priceRoomServicesSet": reservation.price_room_services_set, - "discount": reservation.discount, - "commission": reservation.agency_id.default_commission, - "partnerName": reservation.partner_name, - "adults": reservation.adults, - "pricelist": reservation.pricelist_id.name, - "boardService": ( - reservation.board_service_room_id.pms_board_service_id.name - ) - if reservation.board_service_room_id - else "", - "reservationLines": [] - if not reservation_lines - else reservation_lines, - "folioId": reservation.folio_id.id - if reservation.folio_id - else "", - "saleChannel": reservation.channel_type_id.name - if reservation.channel_type_id - else "", - "externalReference": reservation.external_reference - if reservation.external_reference - else "", - "agency": reservation.agency_id.name - if reservation.agency_id - else "", - "agencyImage": reservation.agency_id.image_1024.decode("utf-8") - if reservation.agency_id - else "", - "state": reservation.state if reservation.state else "", - "roomTypeCode": reservation.room_type_id.default_code - if reservation.room_type_id - else "", - "children": reservation.children if reservation.children else 0, - "countServices": len(reservation.service_ids) - if reservation.service_ids - else 0, - "readyForCheckin": reservation.ready_for_checkin, - "allowedCheckout": reservation.allowed_checkout, - "isSplitted": reservation.splitted, - "arrivalHour": reservation.arrival_hour, - "departureHour": reservation.departure_hour, - "pendingCheckinData": reservation.pending_checkin_data, - "createDate": reservation.create_date, - "segmentationId": segmentation_ids[0] - if segmentation_ids - else 0, - "cancellationPolicy": reservation.pricelist_id.cancelation_rule_id.name - if reservation.pricelist_id.cancelation_rule_id.name - else "", - "pendingAmount": reservation.folio_id.pending_amount, - "toAssign": reservation.to_assign, - "reservationType": reservation.reservation_type, - } - ) + + result_folios.append( PmsFolioInfo( id=folio.id, @@ -188,7 +107,6 @@ def get_folios(self, folio_search_param): folio.state ], pendingAmount=folio.pending_amount, - reservations=[] if not reservations else reservations, salesPerson=folio.user_id.name if folio.user_id else "", paymentState=dict( folio.fields_get(["payment_state"])["payment_state"][ @@ -259,25 +177,91 @@ def get_folio_payments(self, folio_id, pms_search_param): [ ( [ - "/", + "//reservations", ], - "POST", + "GET", ) ], - input_param=Datamodel("pms.reservation.info", is_list=False), + output_param=Datamodel("pms.reservation.info", is_list=True), auth="jwt_api_pms", ) - def create_reservation(self, pms_reservation_info): - reservation = self.env["pms.reservation"].create( - { - "partner_name": pms_reservation_info.partner, - "pms_property_id": pms_reservation_info.pmsPropertyId, - "room_type_id": pms_reservation_info.roomTypeId, - "pricelist_id": pms_reservation_info.pricelistId, - "checkin": pms_reservation_info.checkin, - "checkout": pms_reservation_info.checkout, - "board_service_room_id": pms_reservation_info.boardServiceId, - "channel_type_id": pms_reservation_info.channelTypeId, - } - ) - return reservation.id + def get_folio_reservations(self, folio_id): + folio = self.env["pms.folio"].browse(folio_id) + reservations = [] + PmsReservationInfo = self.env.datamodels["pms.reservation.info"] + if not folio: + pass + else: + if folio.reservation_ids: + for reservation in folio.reservation_ids: + reservations.append( + PmsReservationInfo( + id=reservation.id, + name=reservation.name, + folioId=reservation.folio_id.id, + folioSequence=reservation.folio_sequence, + partnerName=reservation.partner_name, + pmsPropertyId=reservation.pms_property_id.id, + boardServiceId=reservation.board_service_room_id.id or None, + saleChannelId=reservation.channel_type_id.id or None, + agencyId=reservation.agency_id.id or 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 or None, + preferredRoomId=reservation.preferred_room_id.id or None, + pricelistId=reservation.pricelist_id.id, + adults=reservation.adults, + overbooking=reservation.overbooking, + externalReference=reservation.external_reference or None, + state=reservation.state, + children=reservation.children or None, + readyForCheckin=reservation.ready_for_checkin, + allowedCheckout=reservation.allowed_checkout, + isSplitted=reservation.splitted, + pendingCheckinData=reservation.pending_checkin_data, + createDate=datetime.combine(reservation.create_date , datetime.min.time()).isoformat(), + segmentationId=reservation.segmentation_ids[0].id if reservation.segmentation_ids else None, + cancellationPolicyId=reservation.pricelist_id.cancelation_rule_id.id or None, + toAssign=reservation.to_assign, + reservationType=reservation.reservation_type, + priceTotal=reservation.price_room_services_set, + discount=reservation.discount, + commissionAmount=reservation.commission_amount or None, + commissionPercent=reservation.commission_percent or None, + priceOnlyServices=reservation.price_services, + priceOnlyRoom=reservation.price_total, + pendingAmount=reservation.folio_pending_amount, + ) + ) + + return reservations + + + # @restapi.method( + # [ + # ( + # [ + # "/", + # ], + # "POST", + # ) + # ], + # input_param=Datamodel("pms.reservation.info", is_list=False), + # auth="jwt_api_pms", + # ) + # def create_reservation(self, pms_reservation_info): + # reservation = self.env["pms.reservation"].create( + # { + # "partner_name": pms_reservation_info.partner, + # "pms_property_id": pms_reservation_info.pmsPropertyId, + # "room_type_id": pms_reservation_info.roomTypeId, + # "pricelist_id": pms_reservation_info.pricelistId, + # "checkin": pms_reservation_info.checkin, + # "checkout": pms_reservation_info.checkout, + # "board_service_room_id": pms_reservation_info.boardServiceId, + # "channel_type_id": pms_reservation_info.channelTypeId, + # } + # ) + # return reservation.id From 4659057b958a66485014ebb4e768f352f7e7d122 Mon Sep 17 00:00:00 2001 From: Sara Date: Thu, 7 Jul 2022 15:52:22 +0200 Subject: [PATCH 115/547] [IMP] pms_api_rest: add cancelation rule and sale channel services --- pms_api_rest/datamodels/__init__.py | 2 + .../datamodels/pms_cancelation_rule.py | 14 +++ pms_api_rest/datamodels/pms_reservation.py | 5 +- pms_api_rest/datamodels/pms_sale_channel.py | 15 ++++ pms_api_rest/services/__init__.py | 2 + .../services/pms_board_service_service.py | 2 +- pms_api_rest/services/pms_calendar_service.py | 12 +-- .../services/pms_cancelation_rule_service.py | 73 +++++++++++++++ pms_api_rest/services/pms_folio_service.py | 27 +++--- .../services/pms_sale_channel_service.py | 89 +++++++++++++++++++ 10 files changed, 216 insertions(+), 25 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_cancelation_rule.py create mode 100644 pms_api_rest/datamodels/pms_sale_channel.py create mode 100644 pms_api_rest/services/pms_cancelation_rule_service.py create mode 100644 pms_api_rest/services/pms_sale_channel_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index b0bec9988c..334acc6b83 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -39,3 +39,5 @@ from . import pms_board_service_line from . import pms_product +from . import pms_sale_channel +from . import pms_cancelation_rule 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..48e4b364a5 --- /dev/null +++ b/pms_api_rest/datamodels/pms_cancelation_rule.py @@ -0,0 +1,14 @@ +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_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index d305f62ef8..5a0903abff 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -1,4 +1,3 @@ -from sre_parse import State from marshmallow import fields from odoo.addons.datamodel.core import Datamodel @@ -48,5 +47,5 @@ class PmsReservationInfo(Datamodel): pendingAmount = fields.Float(required=False, allow_none=True) # TODO: Refact - #services = fields.List(fields.Dict(required=False, allow_none=True)) - #messages = fields.List(fields.Dict(required=False, allow_none=True)) + # services = fields.List(fields.Dict(required=False, allow_none=True)) + # messages = fields.List(fields.Dict(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..304a271047 --- /dev/null +++ b/pms_api_rest/datamodels/pms_sale_channel.py @@ -0,0 +1,15 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsSaleChannelSearchParam(Datamodel): + _name = "pms.sale.channel.search.param" + id = fields.Integer(required=False, allow_none=True) + pmsPropertyIds = fields.List(fields.Integer(), required=False) + + +class PmsSaleChannelInfo(Datamodel): + _name = "pms.sale.channel.info" + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 5fe03c63f0..b85ed8f6bd 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -25,3 +25,5 @@ from . import pms_board_service_service from . import pms_product_service +from . import pms_sale_channel_service +from . import pms_cancelation_rule_service diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index 51a4e57307..9f261e0aad 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -88,7 +88,7 @@ def get_board_service(self, board_service_id): amount=board_service.amount, ) else: - raise MissingError(_("Amenity Type not found")) + raise MissingError(_("Board Service not found")) @restapi.method( [ diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 2b44deeecc..c0014d6274 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -162,9 +162,7 @@ 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() + date_to = datetime.strptime(pms_calendar_search_param.dateTo, "%Y-%m-%d").date() result = [] PmsCalendarDailyInvoicing = self.env.datamodels["pms.calendar.daily.invoicing"] @@ -203,9 +201,7 @@ 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() + date_to = datetime.strptime(pms_calendar_search_param.dateTo, "%Y-%m-%d").date() result = [] PmsCalendarFreeDailyRoomsByType = self.env.datamodels[ "pms.calendar.free.daily.rooms.by.type" @@ -276,9 +272,7 @@ 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() + date_to = datetime.strptime(pms_calendar_search_param.dateTo, "%Y-%m-%d").date() result = [] for day in ( date_from + timedelta(d) for d in range((date_to - date_from).days + 1) 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..234b79a872 --- /dev/null +++ b/pms_api_rest/services/pms_cancelation_rule_service.py @@ -0,0 +1,73 @@ +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-rule" + _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.append(("pms_property_ids", "in", cancelation_rule_search_param.pmsPropertyId)) + + 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_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d3d0eae710..06e0cedbca 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -29,9 +29,7 @@ class PmsFolioService(Component): def get_folios(self, folio_search_param): domain_fields = list() - domain_fields.append( - ("pms_property_id", "=", folio_search_param.pmsPropertyId) - ) + domain_fields.append(("pms_property_id", "=", folio_search_param.pmsPropertyId)) if folio_search_param.dateTo and folio_search_param.dateFrom: reservation_lines = ( @@ -75,7 +73,6 @@ def get_folios(self, folio_search_param): for folio in self.env["pms.folio"].search( [("id", "in", reservations_result)], ): - reservations = [] for reservation in folio.reservation_ids: reservation_lines = [] for reservation_line in reservation.reservation_line_ids: @@ -90,8 +87,6 @@ def get_folios(self, folio_search_param): } ) - - result_folios.append( PmsFolioInfo( id=folio.id, @@ -205,8 +200,12 @@ def get_folio_reservations(self, folio_id): boardServiceId=reservation.board_service_room_id.id or None, saleChannelId=reservation.channel_type_id.id or None, agencyId=reservation.agency_id.id or None, - checkin=datetime.combine(reservation.checkin, datetime.min.time()).isoformat(), - checkout=datetime.combine(reservation.checkout, datetime.min.time()).isoformat(), + 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 or None, @@ -221,9 +220,14 @@ def get_folio_reservations(self, folio_id): allowedCheckout=reservation.allowed_checkout, isSplitted=reservation.splitted, pendingCheckinData=reservation.pending_checkin_data, - createDate=datetime.combine(reservation.create_date , datetime.min.time()).isoformat(), - segmentationId=reservation.segmentation_ids[0].id if reservation.segmentation_ids else None, - cancellationPolicyId=reservation.pricelist_id.cancelation_rule_id.id or None, + createDate=datetime.combine( + reservation.create_date, datetime.min.time() + ).isoformat(), + segmentationId=reservation.segmentation_ids[0].id + if reservation.segmentation_ids + else None, + cancellationPolicyId=reservation.pricelist_id.cancelation_rule_id.id + or None, toAssign=reservation.to_assign, reservationType=reservation.reservation_type, priceTotal=reservation.price_room_services_set, @@ -238,7 +242,6 @@ def get_folio_reservations(self, folio_id): return reservations - # @restapi.method( # [ # ( 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..73ba38b184 --- /dev/null +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -0,0 +1,89 @@ +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 PmsSaleChannelService(Component): + _inherit = "base.rest.service" + _name = "pms.sale.channel.service" + _usage = "sale-channel" + _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), + ] + + 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, + ) + ) + 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, + ) + else: + raise MissingError(_("Sale Channel not found")) From 7b0bd5f63cdc67534224d3cf2be22bb23bcb048c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 7 Jul 2022 16:05:05 +0200 Subject: [PATCH 116/547] [ADD]pms_api_rest: add rest service agencies --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_agency.py | 14 +++++ pms_api_rest/services/__init__.py | 2 + pms_api_rest/services/pms_agency_service.py | 70 +++++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_agency.py create mode 100644 pms_api_rest/services/pms_agency_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 334acc6b83..5f918af770 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -41,3 +41,4 @@ from . import pms_product from . import pms_sale_channel from . import pms_cancelation_rule +from . import pms_agency diff --git a/pms_api_rest/datamodels/pms_agency.py b/pms_api_rest/datamodels/pms_agency.py new file mode 100644 index 0000000000..045a992ce1 --- /dev/null +++ b/pms_api_rest/datamodels/pms_agency.py @@ -0,0 +1,14 @@ +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) + + +class PmsAgencyInfo(Datamodel): + _name = "pms.agency.info" + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index b85ed8f6bd..78d071844a 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -27,3 +27,5 @@ from . import pms_product_service from . import pms_sale_channel_service from . import pms_cancelation_rule_service + +from . import pms_agency_service 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..959d644b0a --- /dev/null +++ b/pms_api_rest/services/pms_agency_service.py @@ -0,0 +1,70 @@ +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 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.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, + ) + ) + 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, + ) + else: + raise MissingError(_("Agency not found")) From 3875168bdfd80b17fcb17ba8d64504d560fa91cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 7 Jul 2022 16:14:39 +0200 Subject: [PATCH 117/547] [IMP]pms_api_rest: del id field in search datamodels --- pms_api_rest/datamodels/pms_amenity.py | 1 - pms_api_rest/datamodels/pms_amenity_type.py | 1 - pms_api_rest/datamodels/pms_board_service.py | 1 - pms_api_rest/datamodels/pms_extra_bed.py | 1 - pms_api_rest/datamodels/pms_product.py | 1 - pms_api_rest/datamodels/pms_property.py | 2 -- pms_api_rest/datamodels/pms_reservation_line.py | 1 - pms_api_rest/datamodels/pms_room.py | 1 - pms_api_rest/datamodels/pms_room_type.py | 1 - pms_api_rest/datamodels/pms_room_type_class.py | 1 - pms_api_rest/datamodels/pms_sale_channel.py | 1 - pms_api_rest/datamodels/pms_ubication.py | 1 - 12 files changed, 13 deletions(-) diff --git a/pms_api_rest/datamodels/pms_amenity.py b/pms_api_rest/datamodels/pms_amenity.py index c95f244fe4..b9c970c39a 100644 --- a/pms_api_rest/datamodels/pms_amenity.py +++ b/pms_api_rest/datamodels/pms_amenity.py @@ -5,7 +5,6 @@ class PmsAmenitySearchParam(Datamodel): _name = "pms.amenity.search.param" - id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) pmsPropertyId = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_amenity_type.py b/pms_api_rest/datamodels/pms_amenity_type.py index 1de011612f..86af7e082d 100644 --- a/pms_api_rest/datamodels/pms_amenity_type.py +++ b/pms_api_rest/datamodels/pms_amenity_type.py @@ -5,7 +5,6 @@ class PmsAmenityTypeSearchParam(Datamodel): _name = "pms.amenity.type.search.param" - id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) pmsPropertyId = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_board_service.py b/pms_api_rest/datamodels/pms_board_service.py index ebf27b4b3c..5e8b8138b2 100644 --- a/pms_api_rest/datamodels/pms_board_service.py +++ b/pms_api_rest/datamodels/pms_board_service.py @@ -5,7 +5,6 @@ class PmsBoardServiceSearchParam(Datamodel): _name = "pms.board.service.search.param" - ids = fields.List(fields.Integer(required=False, allow_none=True)) name = fields.String(required=False, allow_none=True) pmsPropertyId = fields.Integer(required=False, allow_none=True) roomTypeId = fields.Integer(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 index 9cfc9f7f22..7d63e8ee15 100644 --- a/pms_api_rest/datamodels/pms_extra_bed.py +++ b/pms_api_rest/datamodels/pms_extra_bed.py @@ -5,7 +5,6 @@ class PmsExtraBedSearchParam(Datamodel): _name = "pms.extra.beds.search.param" - id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) pmsPropertyId = fields.Integer(required=True, allow_none=False) dateFrom = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_product.py b/pms_api_rest/datamodels/pms_product.py index 7ceabe03d0..60bd6a202e 100644 --- a/pms_api_rest/datamodels/pms_product.py +++ b/pms_api_rest/datamodels/pms_product.py @@ -5,7 +5,6 @@ class PmsProductSearchParam(Datamodel): _name = "pms.product.search.param" - ids = fields.List(fields.Integer(required=False, allow_none=True)) name = fields.String(required=False, allow_none=True) pmsPropertyId = fields.Integer(required=True, allow_none=False) pricelistId = fields.Integer(required=True, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index f5f556c231..40e4d45509 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -5,8 +5,6 @@ class PmsPropertySearchParam(Datamodel): _name = "pms.property.search.param" - - id = fields.Integer(required=False, allow_none=False) name = fields.String(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 index f92d02ea6e..1deeb03b20 100644 --- a/pms_api_rest/datamodels/pms_reservation_line.py +++ b/pms_api_rest/datamodels/pms_reservation_line.py @@ -5,7 +5,6 @@ class PmsReservationLineSearchParam(Datamodel): _name = "pms.reservation.line.search.param" - id = fields.Integer(required=False, allow_none=False) date = fields.String(required=False, allow_none=False) reservationId = fields.Integer(required=False, allow_none=False) pmsPropertyId = fields.Integer(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index 987d9e770f..da9eb54575 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -5,7 +5,6 @@ class PmsRoomSearchParam(Datamodel): _name = "pms.room.search.param" - id = fields.Integer(required=False, allow_none=False) name = fields.String(required=False, allow_none=False) pmsPropertyId = fields.Integer(required=True, allow_none=False) availabilityFrom = fields.String(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_room_type.py b/pms_api_rest/datamodels/pms_room_type.py index 9c3645c22f..314a143c5b 100644 --- a/pms_api_rest/datamodels/pms_room_type.py +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -5,7 +5,6 @@ class PmsRoomTypeSearchParam(Datamodel): _name = "pms.room.type.search.param" - 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_room_type_class.py b/pms_api_rest/datamodels/pms_room_type_class.py index b358661738..43bd8d401f 100644 --- a/pms_api_rest/datamodels/pms_room_type_class.py +++ b/pms_api_rest/datamodels/pms_room_type_class.py @@ -5,7 +5,6 @@ class PmsRoomTypeClassSearchParam(Datamodel): _name = "pms.room.type.class.search.param" - 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_sale_channel.py b/pms_api_rest/datamodels/pms_sale_channel.py index 304a271047..77a82e5d88 100644 --- a/pms_api_rest/datamodels/pms_sale_channel.py +++ b/pms_api_rest/datamodels/pms_sale_channel.py @@ -5,7 +5,6 @@ class PmsSaleChannelSearchParam(Datamodel): _name = "pms.sale.channel.search.param" - id = fields.Integer(required=False, allow_none=True) pmsPropertyIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_ubication.py b/pms_api_rest/datamodels/pms_ubication.py index 8e7a601181..03f1c51582 100644 --- a/pms_api_rest/datamodels/pms_ubication.py +++ b/pms_api_rest/datamodels/pms_ubication.py @@ -5,7 +5,6 @@ class PmsUbicationSearchParam(Datamodel): _name = "pms.ubication.search.param" - id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) pmsPropertyIds = fields.List(fields.Integer(), required=False) From c8352976c80391568d2bd17647bedec04f15540a Mon Sep 17 00:00:00 2001 From: Sara Date: Thu, 7 Jul 2022 16:20:07 +0200 Subject: [PATCH 118/547] [IMP]pms_api_rest: del id field in input params --- pms_api_rest/services/pms_amenity_service.py | 2 -- pms_api_rest/services/pms_amenity_type_service.py | 2 -- pms_api_rest/services/pms_board_service_service.py | 2 -- pms_api_rest/services/pms_extra_beds_service.py | 2 -- pms_api_rest/services/pms_product_service.py | 2 -- pms_api_rest/services/pms_reservation_line_service.py | 2 -- pms_api_rest/services/pms_room_service.py | 2 -- 7 files changed, 14 deletions(-) diff --git a/pms_api_rest/services/pms_amenity_service.py b/pms_api_rest/services/pms_amenity_service.py index a722c3b945..395100cf05 100644 --- a/pms_api_rest/services/pms_amenity_service.py +++ b/pms_api_rest/services/pms_amenity_service.py @@ -29,8 +29,6 @@ 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.id: - domain.append(("id", "=", amenities_search_param.id)) if amenities_search_param.pmsPropertyId: domain.extend( [ diff --git a/pms_api_rest/services/pms_amenity_type_service.py b/pms_api_rest/services/pms_amenity_type_service.py index 18a4323c95..cd9f7e0dac 100644 --- a/pms_api_rest/services/pms_amenity_type_service.py +++ b/pms_api_rest/services/pms_amenity_type_service.py @@ -29,8 +29,6 @@ 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.id: - domain.append(("id", "=", amenity_types_search_param.id)) if amenity_types_search_param.pmsPropertyId: domain.extend( [ diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index 9f261e0aad..ba66ec40c4 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -29,8 +29,6 @@ def get_board_services(self, board_services_search_param): domain = [] if board_services_search_param.name: domain.append(("name", "like", board_services_search_param.name)) - if board_services_search_param.ids: - domain.append(("id", "in", board_services_search_param.ids)) if board_services_search_param.roomTypeId: domain.append( ("pms_room_type_id", "=", board_services_search_param.roomTypeId) diff --git a/pms_api_rest/services/pms_extra_beds_service.py b/pms_api_rest/services/pms_extra_beds_service.py index a5bfca4326..f9ad835363 100644 --- a/pms_api_rest/services/pms_extra_beds_service.py +++ b/pms_api_rest/services/pms_extra_beds_service.py @@ -26,8 +26,6 @@ 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.id: - domain.append(("id", "=", extra_beds_search_param.id)) if extra_beds_search_param.pmsPropertyId: domain.extend( [ diff --git a/pms_api_rest/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py index d4ee084898..7372189288 100644 --- a/pms_api_rest/services/pms_product_service.py +++ b/pms_api_rest/services/pms_product_service.py @@ -31,8 +31,6 @@ def get_products(self, product_search_param): domain = [("sale_ok", "=", True)] if product_search_param.name: domain.append(("name", "like", product_search_param.name)) - if product_search_param.ids: - domain.append(("id", "in", product_search_param.ids)) if product_search_param.pmsPropertyId: domain.extend( [ diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index 56c51e37ff..22c2983eed 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -64,8 +64,6 @@ 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.id: - domain.append(("id", "=", reservation_lines_search_param.id)) if reservation_lines_search_param.reservationId: domain.append( ("reservation_id", "=", reservation_lines_search_param.reservationId) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 6d5e200c71..793a4bc459 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -31,8 +31,6 @@ 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.id: - domain.append(("id", "=", room_search_param.id)) if room_search_param.pmsPropertyId: domain.append(("pms_property_id", "=", room_search_param.pmsPropertyId)) if ( From 98a2ea470452cf6f65e0ee0eb87d634ee89e11db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 7 Jul 2022 16:39:09 +0200 Subject: [PATCH 119/547] [IMP]pms_api_rest: Add service reservation/reservation-lines --- pms_api_rest/services/pms_folio_service.py | 14 -- .../services/pms_reservation_service.py | 169 +++++++++++------- 2 files changed, 106 insertions(+), 77 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 06e0cedbca..d104702a28 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -73,20 +73,6 @@ def get_folios(self, folio_search_param): for folio in self.env["pms.folio"].search( [("id", "in", reservations_result)], ): - for reservation in folio.reservation_ids: - reservation_lines = [] - for reservation_line in reservation.reservation_line_ids: - reservation_lines.append( - { - "id": reservation_line.id, - "date": datetime.combine( - reservation_line.date, datetime.min.time() - ).isoformat(), - "roomId": reservation_line.room_id.id, - "roomName": reservation_line.room_id.name, - } - ) - result_folios.append( PmsFolioInfo( id=folio.id, diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index c09954a7f4..03725aea79 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -1,4 +1,7 @@ from datetime import datetime, timedelta +from odoo.exceptions import MissingError +from odoo import _ + from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -35,75 +38,80 @@ def get_reservation(self, reservation_id, pms_search_param): if not reservation: pass else: - services = [] - for service in reservation.service_ids: - if service.is_board_service: - services.append( - { - "id": service.id, - "name": service.name, - "quantity": service.product_qty, - "priceTotal": service.price_total, - "priceSubtotal": service.price_subtotal, - "priceTaxes": service.price_tax, - "discount": service.discount, - } - ) - messages = [] - import re + # services = [] + # for service in reservation.service_ids: + # if service.is_board_service: + # services.append( + # { + # "id": service.id, + # "name": service.name, + # "quantity": service.product_qty, + # "priceTotal": service.price_total, + # "priceSubtotal": service.price_subtotal, + # "priceTaxes": service.price_tax, + # "discount": service.discount, + # } + # ) + # 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), - } - ) + # 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, - partner=reservation.partner_id.name if reservation.partner_id else "", - checkin=str(reservation.checkin), - checkout=str(reservation.checkout), - preferredRoomId=reservation.preferred_room_id.id - if reservation.preferred_room_id - else 0, - preferredRoomName=reservation.preferred_room_id.name - if reservation.preferred_room_id - else "", - roomTypeId=reservation.room_type_id.id - if reservation.room_type_id - else 0, - roomTypeName=reservation.room_type_id.name - if reservation.room_type_id - else "", name=reservation.name, + folioId=reservation.folio_id.id, + folioSequence=reservation.folio_sequence, + partnerName=reservation.partner_name, + pmsPropertyId=reservation.pms_property_id.id, + boardServiceId=reservation.board_service_room_id.id or None, + saleChannelId=reservation.channel_type_id.id or None, + agencyId=reservation.agency_id.id or 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 or None, + preferredRoomId=reservation.preferred_room_id.id or None, + pricelistId=reservation.pricelist_id.id, + adults=reservation.adults, + overbooking=reservation.overbooking, + externalReference=reservation.external_reference or None, + state=reservation.state, + children=reservation.children or None, + readyForCheckin=reservation.ready_for_checkin, + allowedCheckout=reservation.allowed_checkout, + isSplitted=reservation.splitted, + pendingCheckinData=reservation.pending_checkin_data, + createDate=datetime.combine( + reservation.create_date, datetime.min.time() + ).isoformat(), + segmentationId=reservation.segmentation_ids[0].id + if reservation.segmentation_ids + else None, + cancellationPolicyId=reservation.pricelist_id.cancelation_rule_id.id + or None, + toAssign=reservation.to_assign, + reservationType=reservation.reservation_type, priceTotal=reservation.price_room_services_set, - priceOnlyServices=reservation.price_services - if reservation.price_services - else 0.0, + discount=reservation.discount, + commissionAmount=reservation.commission_amount or None, + commissionPercent=reservation.commission_percent or None, + priceOnlyServices=reservation.price_services, priceOnlyRoom=reservation.price_total, - pricelistName=reservation.pricelist_id.name - if reservation.pricelist_id - else "", - pricelistId=reservation.pricelist_id.id - if reservation.pricelist_id - else 0, - services=services if services else [], - messages=messages, - boardServiceId=reservation.board_service_room_id.id - if reservation.board_service_room_id - else 0, - boardServiceName=reservation.board_service_room_id.pms_board_service_id.name - if reservation.board_service_room_id - else "", - # review if its an agency - channelTypeId=reservation.channel_type_id.id - if reservation.channel_type_id - else 0, - adults=reservation.adults, + pendingAmount=reservation.folio_pending_amount, ) return res @@ -208,6 +216,41 @@ def update_reservation(self, reservation_id, reservation_lines_changes): ) reservation_to_update.write(reservation_vals) + @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=reservation_line.price, + discount=reservation_line.discount, + cancelDiscount=reservation_line.cancel_discount, + roomId=reservation_line.room_id.id, + reservationId=reservation_line.reservation_id.id, + pmsPropertyId=reservation_line.pms_property_id.id, + ) + ) + return result_lines + @restapi.method( [ ( From 1bfe015ae48ec410c615a6c9e3419c218b0c75a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 7 Jul 2022 18:51:14 +0200 Subject: [PATCH 120/547] [IMP]pms_api_rest: Add datamodel folio short info --- pms_api_rest/datamodels/pms_folio.py | 17 +++ pms_api_rest/services/pms_folio_service.py | 155 +++++++++++++++++---- 2 files changed, 147 insertions(+), 25 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index d1de818ac2..262d2ae7d1 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -19,6 +19,22 @@ class PmsFolioInfo(Datamodel): partnerName = fields.String(required=False, allow_none=True) partnerPhone = fields.String(required=False, allow_none=True) partnerEmail = fields.String(required=False, allow_none=True) + saleChannelId = fields.Integer(required=False, allow_none=True) + agencyId = fields.Integer(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + pendingAmount = fields.Float(required=False, allow_none=True) + salesPersonId = fields.Integer(required=False, allow_none=True) + paymentState = fields.String(required=False, allow_none=True) + propertyId = fields.Integer(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) + partnerName = fields.String(required=False, allow_none=True) + partnerPhone = fields.String(required=False, allow_none=True) + partnerEmail = fields.String(required=False, allow_none=True) saleChannel = fields.String(required=False, allow_none=True) agency = fields.String(required=False, allow_none=True) state = fields.String(required=False, allow_none=True) @@ -27,3 +43,4 @@ class PmsFolioInfo(Datamodel): paymentState = fields.String(required=False, allow_none=True) propertyId = fields.Integer(required=False, allow_none=True) agencyImage = fields.String(required=False, allow_none=True) + reservations = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d104702a28..cc18e88e0e 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1,6 +1,9 @@ -from datetime import datetime - +from datetime import datetime, timedelta +from odoo import _, fields +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo.osv import expression +from odoo.exceptions import MissingError + from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -13,6 +16,49 @@ class PmsFolioService(Component): _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: + PmsFolioInfo = self.env.datamodels["pms.folio.info"] + return PmsFolioInfo( + id=folio.id, + name=folio.name, + 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, + saleChannelId=folio.channel_type_id.id + if folio.channel_type_id + else None, + agencyId=folio.agency_id.id if folio.agency_id else None, + state=dict(folio.fields_get(["state"])["state"]["selection"])[ + folio.state + ], + pendingAmount=folio.pending_amount, + salesPersonId=folio.user_id.id if folio.user_id else None, + paymentState=dict( + folio.fields_get(["payment_state"])["payment_state"][ + "selection" + ] + )[folio.payment_state], + propertyId=folio.pms_property_id.id, + ) + else: + raise MissingError(_("Folio not found")) + @restapi.method( [ ( @@ -23,27 +69,30 @@ class PmsFolioService(Component): ) ], input_param=Datamodel("pms.folio.search.param"), - output_param=Datamodel("pms.folio.info", is_list=True), + output_param=Datamodel("pms.folio.short.info", is_list=True), auth="jwt_api_pms", ) def get_folios(self, folio_search_param): domain_fields = list() - domain_fields.append(("pms_property_id", "=", folio_search_param.pmsPropertyId)) + domain_fields.append( + ("pms_property_id", "=", folio_search_param.pmsPropertyId) + ) if folio_search_param.dateTo and folio_search_param.dateFrom: - reservation_lines = ( + 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) + ] + reservation_lines = list(set( self.env["pms.reservation.line"] - .search( - [ - ("date", ">=", folio_search_param.dateFrom), - ("date", "<", folio_search_param.dateTo), - ] - ) + .search([("date", "in", dates)]) .mapped("reservation_id") .mapped("folio_id") .ids - ) + )) domain_fields.append(("folio_id", "in", reservation_lines)) domain_filter = list() @@ -69,35 +118,89 @@ def get_folios(self, folio_search_param): self.env["pms.reservation"].search(domain).mapped("folio_id").ids ) - PmsFolioInfo = self.env.datamodels["pms.folio.info"] + PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] for folio in self.env["pms.folio"].search( [("id", "in", reservations_result)], ): + reservations = [] + for reservation in folio.reservation_ids: + reservations.append( + { + "id": reservation.id, + "name": reservation.name, + "folioSequence": reservation.folio_sequence, + "checkin": datetime.combine( + reservation.checkin, datetime.min.time() + ).isoformat(), + "checkout": datetime.combine( + reservation.checkout, datetime.min.time() + ).isoformat(), + "preferredRoomId": reservation.preferred_room_id.id + if reservation.preferred_room_id + else None, + "roomTypeName": reservation.room_type_id.name + if reservation.room_type_id + else None, + "adults": reservation.adults, + "pricelist": reservation.pricelist_id.name, + "boardService": ( + reservation.board_service_room_id.pms_board_service_id.name + ) + if reservation.board_service_room_id + else None, + "saleChannel": reservation.channel_type_id.name + if reservation.channel_type_id + else None, + "agency": reservation.agency_id.name + if reservation.agency_id + else None, + "agencyImage": reservation.agency_id.image_1024.decode("utf-8") + if reservation.agency_id + else None, + "state": reservation.state if reservation.state else None, + "roomTypeCode": reservation.room_type_id.default_code + if reservation.room_type_id + else None, + "children": reservation.children if reservation.children else None, + "countServices": len(reservation.service_ids) + if reservation.service_ids + else None, + "readyForCheckin": reservation.ready_for_checkin, + "allowedCheckout": reservation.allowed_checkout, + "isSplitted": reservation.splitted, + "arrivalHour": reservation.arrival_hour, + "departureHour": reservation.departure_hour, + "pendingCheckinData": reservation.pending_checkin_data, + "toAssign": reservation.to_assign, + "reservationType": reservation.reservation_type, + } + ) result_folios.append( - PmsFolioInfo( + PmsFolioShortInfo( id=folio.id, name=folio.name, - partnerName=folio.partner_name if folio.partner_name else "", - partnerPhone=folio.mobile if folio.mobile else "", - partnerEmail=folio.email if folio.email else "", + 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, saleChannel=folio.channel_type_id.name if folio.channel_type_id - else "", - agency=folio.agency_id.name if folio.agency_id else "", + else None, + agency=folio.agency_id.name if folio.agency_id else None, state=dict(folio.fields_get(["state"])["state"]["selection"])[ folio.state ], pendingAmount=folio.pending_amount, - salesPerson=folio.user_id.name if folio.user_id else "", + reservations=[] if not reservations else reservations, + salesPerson=folio.user_id.name if folio.user_id else None, paymentState=dict( folio.fields_get(["payment_state"])["payment_state"][ "selection" ] )[folio.payment_state] if folio.payment_state - else "", + else None, propertyId=folio.pms_property_id, - agencyImage=folio.agency_id.image_1024 if folio.agency_id else "", + agencyImage=folio.agency_id.image_1024 if folio.agency_id else None, ) ) return result_folios @@ -106,7 +209,7 @@ def get_folios(self, folio_search_param): [ ( [ - "//payments", + "//payments", ], "GET", ) @@ -158,7 +261,7 @@ def get_folio_payments(self, folio_id, pms_search_param): [ ( [ - "//reservations", + "//reservations", ], "GET", ) @@ -200,7 +303,9 @@ def get_folio_reservations(self, folio_id): adults=reservation.adults, overbooking=reservation.overbooking, externalReference=reservation.external_reference or None, - state=reservation.state, + state=dict(reservation.fields_get(["state"])["state"]["selection"])[ + reservation.state + ], children=reservation.children or None, readyForCheckin=reservation.ready_for_checkin, allowedCheckout=reservation.allowed_checkout, From 1109bbfb3a705dbf98ec89ab5f0878a2871a89a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 7 Jul 2022 19:01:51 +0200 Subject: [PATCH 121/547] [FIX]pms_api_rest: Agency without image --- pms_api_rest/datamodels/pms_cancelation_rule.py | 1 + pms_api_rest/services/pms_folio_service.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_cancelation_rule.py b/pms_api_rest/datamodels/pms_cancelation_rule.py index 48e4b364a5..a2959dfe77 100644 --- a/pms_api_rest/datamodels/pms_cancelation_rule.py +++ b/pms_api_rest/datamodels/pms_cancelation_rule.py @@ -8,6 +8,7 @@ class PmsCancelationRuleSearchParam(Datamodel): 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) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index cc18e88e0e..f4fab02c55 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -155,7 +155,7 @@ def get_folios(self, folio_search_param): if reservation.agency_id else None, "agencyImage": reservation.agency_id.image_1024.decode("utf-8") - if reservation.agency_id + if reservation.agency_id and reservation.agency_id.image_1024 else None, "state": reservation.state if reservation.state else None, "roomTypeCode": reservation.room_type_id.default_code @@ -200,7 +200,9 @@ def get_folios(self, folio_search_param): if folio.payment_state else None, propertyId=folio.pms_property_id, - agencyImage=folio.agency_id.image_1024 if folio.agency_id else None, + agencyImage=folio.agency_id.image_1024 + if folio.agency_id and folio.agency_id.image_1024 + else None, ) ) return result_folios From 9a97fa48ff3507ca18ac88e5a83e011c3fc25dfc Mon Sep 17 00:00:00 2001 From: Sara Date: Fri, 8 Jul 2022 13:16:29 +0200 Subject: [PATCH 122/547] [IMP]pms_api_rest: update fields in folio and reservation datamodels --- pms_api_rest/datamodels/pms_folio.py | 16 +- pms_api_rest/datamodels/pms_reservation.py | 21 ++- pms_api_rest/services/pms_agency_service.py | 10 +- .../services/pms_cancelation_rule_service.py | 9 +- pms_api_rest/services/pms_folio_service.py | 138 ++++-------------- .../services/pms_reservation_service.py | 12 +- 6 files changed, 67 insertions(+), 139 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 262d2ae7d1..11b9b07f06 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -19,28 +19,16 @@ class PmsFolioInfo(Datamodel): partnerName = fields.String(required=False, allow_none=True) partnerPhone = fields.String(required=False, allow_none=True) partnerEmail = fields.String(required=False, allow_none=True) - saleChannelId = fields.Integer(required=False, allow_none=True) - agencyId = fields.Integer(required=False, allow_none=True) state = fields.String(required=False, allow_none=True) - pendingAmount = fields.Float(required=False, allow_none=True) - salesPersonId = fields.Integer(required=False, allow_none=True) - paymentState = fields.String(required=False, allow_none=True) - propertyId = fields.Integer(required=False, allow_none=True) + amountTotal = fields.Float(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) partnerName = fields.String(required=False, allow_none=True) partnerPhone = fields.String(required=False, allow_none=True) partnerEmail = fields.String(required=False, allow_none=True) - saleChannel = fields.String(required=False, allow_none=True) - agency = fields.String(required=False, allow_none=True) - state = fields.String(required=False, allow_none=True) - pendingAmount = fields.Float(required=False, allow_none=True) - salesPerson = fields.String(required=False, allow_none=True) + amountTotal = fields.Float(required=False, allow_none=True) paymentState = fields.String(required=False, allow_none=True) - propertyId = fields.Integer(required=False, allow_none=True) - agencyImage = fields.String(required=False, allow_none=True) reservations = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 5a0903abff..3efc32ed72 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -3,14 +3,29 @@ from odoo.addons.datamodel.core import Datamodel +class PmsReservationShortInfo(Datamodel): + _name = "pms.reservation.short.info" + id = fields.Integer(required=False, allow_none=True) + boardServiceName = fields.String(required=False, allow_none=True) + checkin = fields.String(required=False, allow_none=True) + checkout = fields.String(required=False, allow_none=True) + roomTypeName = fields.String(required=False, allow_none=True) + preferredRoomShortName = fields.String(required=False, allow_none=True) + adults = fields.Integer(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + children = fields.Integer(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) + + 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) partnerName = fields.String(required=False, allow_none=True) - pmsPropertyId = fields.Integer(required=False, allow_none=True) boardServiceId = fields.Integer(required=False, allow_none=True) saleChannelId = fields.Integer(required=False, allow_none=True) agencyId = fields.Integer(required=False, allow_none=True) @@ -41,10 +56,8 @@ class PmsReservationInfo(Datamodel): priceTotal = fields.Float(required=False, allow_none=True) discount = 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) - pendingAmount = fields.Float(required=False, allow_none=True) # TODO: Refact # services = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/pms_agency_service.py b/pms_api_rest/services/pms_agency_service.py index 959d644b0a..52298eb8ec 100644 --- a/pms_api_rest/services/pms_agency_service.py +++ b/pms_api_rest/services/pms_agency_service.py @@ -56,10 +56,12 @@ def get_agencies(self, agencies_search_param): auth="jwt_api_pms", ) def get_agency(self, agency_id): - agency = self.env["res.partner"].search([ - ("id", "=", agency_id), - ("is_agency", "=", True), - ]) + agency = self.env["res.partner"].search( + [ + ("id", "=", agency_id), + ("is_agency", "=", True), + ] + ) if agency: PmsAgencieInfo = self.env.datamodels["pms.agency.info"] return PmsAgencieInfo( diff --git a/pms_api_rest/services/pms_cancelation_rule_service.py b/pms_api_rest/services/pms_cancelation_rule_service.py index 234b79a872..96842bd422 100644 --- a/pms_api_rest/services/pms_cancelation_rule_service.py +++ b/pms_api_rest/services/pms_cancelation_rule_service.py @@ -29,9 +29,13 @@ 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)) + domain.append( + ("pricelist_ids", "in", cancelation_rule_search_param.pricelistId) + ) if cancelation_rule_search_param.pmsPropertyId: - domain.append(("pms_property_ids", "in", cancelation_rule_search_param.pmsPropertyId)) + domain.append( + ("pms_property_ids", "in", cancelation_rule_search_param.pmsPropertyId) + ) result_cancelation_rules = [] PmsCancelationRuleInfo = self.env.datamodels["pms.cancelation.rule.info"] @@ -70,4 +74,3 @@ def get_cancelation_rule(self, cancelation_rule_id): ) else: raise MissingError(_("Cancelation Rule not found")) - diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index f4fab02c55..a2c70a575b 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1,9 +1,8 @@ from datetime import datetime, timedelta + from odoo import _, fields -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT -from odoo.osv import expression from odoo.exceptions import MissingError - +from odoo.osv import expression from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -29,9 +28,11 @@ class PmsFolioService(Component): auth="jwt_api_pms", ) def get_folio(self, folio_id): - folio = self.env["pms.folio"].search([ - ("id", "=", folio_id), - ]) + folio = self.env["pms.folio"].search( + [ + ("id", "=", folio_id), + ] + ) if folio: PmsFolioInfo = self.env.datamodels["pms.folio.info"] return PmsFolioInfo( @@ -40,21 +41,10 @@ def get_folio(self, folio_id): 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, - saleChannelId=folio.channel_type_id.id - if folio.channel_type_id - else None, - agencyId=folio.agency_id.id if folio.agency_id else None, state=dict(folio.fields_get(["state"])["state"]["selection"])[ folio.state ], - pendingAmount=folio.pending_amount, - salesPersonId=folio.user_id.id if folio.user_id else None, - paymentState=dict( - folio.fields_get(["payment_state"])["payment_state"][ - "selection" - ] - )[folio.payment_state], - propertyId=folio.pms_property_id.id, + amountTotal=folio.amount_total, ) else: raise MissingError(_("Folio not found")) @@ -75,9 +65,7 @@ def get_folio(self, folio_id): def get_folios(self, folio_search_param): domain_fields = list() - domain_fields.append( - ("pms_property_id", "=", folio_search_param.pmsPropertyId) - ) + domain_fields.append(("pms_property_id", "=", folio_search_param.pmsPropertyId)) if folio_search_param.dateTo and folio_search_param.dateFrom: date_from = fields.Date.from_string(folio_search_param.dateFrom) @@ -86,13 +74,15 @@ def get_folios(self, folio_search_param): date_from + timedelta(days=x) for x in range(0, (date_to - date_from).days + 1) ] - reservation_lines = list(set( - self.env["pms.reservation.line"] - .search([("date", "in", dates)]) - .mapped("reservation_id") - .mapped("folio_id") - .ids - )) + reservation_lines = list( + set( + self.env["pms.reservation.line"] + .search([("date", "in", dates)]) + .mapped("reservation_id") + .mapped("folio_id") + .ids + ) + ) domain_fields.append(("folio_id", "in", reservation_lines)) domain_filter = list() @@ -127,27 +117,17 @@ def get_folios(self, folio_search_param): reservations.append( { "id": reservation.id, - "name": reservation.name, - "folioSequence": reservation.folio_sequence, "checkin": datetime.combine( reservation.checkin, datetime.min.time() ).isoformat(), "checkout": datetime.combine( reservation.checkout, datetime.min.time() ).isoformat(), - "preferredRoomId": reservation.preferred_room_id.id + "preferredRoomShortName": reservation.preferred_room_id.short_name if reservation.preferred_room_id else None, - "roomTypeName": reservation.room_type_id.name - if reservation.room_type_id - else None, "adults": reservation.adults, - "pricelist": reservation.pricelist_id.name, - "boardService": ( - reservation.board_service_room_id.pms_board_service_id.name - ) - if reservation.board_service_room_id - else None, + "pricelistName": reservation.pricelist_id.name, "saleChannel": reservation.channel_type_id.name if reservation.channel_type_id else None, @@ -157,41 +137,16 @@ def get_folios(self, folio_search_param): "agencyImage": reservation.agency_id.image_1024.decode("utf-8") if reservation.agency_id and reservation.agency_id.image_1024 else None, - "state": reservation.state if reservation.state else None, - "roomTypeCode": reservation.room_type_id.default_code - if reservation.room_type_id - else None, - "children": reservation.children if reservation.children else None, - "countServices": len(reservation.service_ids) - if reservation.service_ids - else None, - "readyForCheckin": reservation.ready_for_checkin, - "allowedCheckout": reservation.allowed_checkout, - "isSplitted": reservation.splitted, - "arrivalHour": reservation.arrival_hour, - "departureHour": reservation.departure_hour, - "pendingCheckinData": reservation.pending_checkin_data, - "toAssign": reservation.to_assign, - "reservationType": reservation.reservation_type, } ) result_folios.append( PmsFolioShortInfo( id=folio.id, - name=folio.name, 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, - saleChannel=folio.channel_type_id.name - if folio.channel_type_id - else None, - agency=folio.agency_id.name if folio.agency_id else None, - state=dict(folio.fields_get(["state"])["state"]["selection"])[ - folio.state - ], - pendingAmount=folio.pending_amount, + amountTotal=folio.amount_total, reservations=[] if not reservations else reservations, - salesPerson=folio.user_id.name if folio.user_id else None, paymentState=dict( folio.fields_get(["payment_state"])["payment_state"][ "selection" @@ -199,10 +154,6 @@ def get_folios(self, folio_search_param): )[folio.payment_state] if folio.payment_state else None, - propertyId=folio.pms_property_id, - agencyImage=folio.agency_id.image_1024 - if folio.agency_id and folio.agency_id.image_1024 - else None, ) ) return result_folios @@ -274,62 +225,35 @@ def get_folio_payments(self, folio_id, pms_search_param): def get_folio_reservations(self, folio_id): folio = self.env["pms.folio"].browse(folio_id) reservations = [] - PmsReservationInfo = self.env.datamodels["pms.reservation.info"] + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] if not folio: pass else: if folio.reservation_ids: for reservation in folio.reservation_ids: reservations.append( - PmsReservationInfo( + PmsReservationShortInfo( id=reservation.id, - name=reservation.name, - folioId=reservation.folio_id.id, - folioSequence=reservation.folio_sequence, - partnerName=reservation.partner_name, - pmsPropertyId=reservation.pms_property_id.id, - boardServiceId=reservation.board_service_room_id.id or None, - saleChannelId=reservation.channel_type_id.id or None, - agencyId=reservation.agency_id.id or None, + boardServiceName=reservation.board_service_room_id.pms_board_service_id.name + or 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 or None, - preferredRoomId=reservation.preferred_room_id.id or None, - pricelistId=reservation.pricelist_id.id, + roomTypeName=reservation.room_type_id.name or None, + preferredRoomShortName=reservation.preferred_room_id.short_name + or None, adults=reservation.adults, - overbooking=reservation.overbooking, - externalReference=reservation.external_reference or None, - state=dict(reservation.fields_get(["state"])["state"]["selection"])[ - reservation.state - ], + state=dict( + reservation.fields_get(["state"])["state"]["selection"] + )[reservation.state], children=reservation.children or None, readyForCheckin=reservation.ready_for_checkin, allowedCheckout=reservation.allowed_checkout, isSplitted=reservation.splitted, - pendingCheckinData=reservation.pending_checkin_data, - createDate=datetime.combine( - reservation.create_date, datetime.min.time() - ).isoformat(), - segmentationId=reservation.segmentation_ids[0].id - if reservation.segmentation_ids - else None, - cancellationPolicyId=reservation.pricelist_id.cancelation_rule_id.id - or None, - toAssign=reservation.to_assign, - reservationType=reservation.reservation_type, priceTotal=reservation.price_room_services_set, - discount=reservation.discount, - commissionAmount=reservation.commission_amount or None, - commissionPercent=reservation.commission_percent or None, - priceOnlyServices=reservation.price_services, - priceOnlyRoom=reservation.price_total, - pendingAmount=reservation.folio_pending_amount, ) ) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 03725aea79..b170ad51c3 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta -from odoo.exceptions import MissingError -from odoo import _ +from odoo import _ +from odoo.exceptions import MissingError from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -69,9 +69,7 @@ def get_reservation(self, reservation_id, pms_search_param): id=reservation.id, name=reservation.name, folioId=reservation.folio_id.id, - folioSequence=reservation.folio_sequence, partnerName=reservation.partner_name, - pmsPropertyId=reservation.pms_property_id.id, boardServiceId=reservation.board_service_room_id.id or None, saleChannelId=reservation.channel_type_id.id or None, agencyId=reservation.agency_id.id or None, @@ -89,7 +87,9 @@ def get_reservation(self, reservation_id, pms_search_param): adults=reservation.adults, overbooking=reservation.overbooking, externalReference=reservation.external_reference or None, - state=reservation.state, + state=dict(reservation.fields_get(["state"])["state"]["selection"])[ + reservation.state + ], children=reservation.children or None, readyForCheckin=reservation.ready_for_checkin, allowedCheckout=reservation.allowed_checkout, @@ -108,10 +108,8 @@ def get_reservation(self, reservation_id, pms_search_param): priceTotal=reservation.price_room_services_set, discount=reservation.discount, commissionAmount=reservation.commission_amount or None, - commissionPercent=reservation.commission_percent or None, priceOnlyServices=reservation.price_services, priceOnlyRoom=reservation.price_total, - pendingAmount=reservation.folio_pending_amount, ) return res From 0898891a5829ab385d494170eb3cb5cae3e9cab6 Mon Sep 17 00:00:00 2001 From: Sara Date: Mon, 11 Jul 2022 11:14:35 +0200 Subject: [PATCH 123/547] [IMP]pms_api_rest: add services in reservation service and service lines service --- pms_api_rest/datamodels/__init__.py | 2 + pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/datamodels/pms_service.py | 14 +++++ pms_api_rest/datamodels/pms_service_line.py | 14 +++++ pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/pms_folio_service.py | 5 ++ .../services/pms_reservation_service.py | 32 ++++++++++++ .../services/pms_service_line_service.py | 52 +++++++++++++++++++ 8 files changed, 121 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_service.py create mode 100644 pms_api_rest/datamodels/pms_service_line.py create mode 100644 pms_api_rest/services/pms_service_line_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 5f918af770..ddf81cb59d 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -42,3 +42,5 @@ from . import pms_sale_channel from . import pms_cancelation_rule from . import pms_agency +from . import pms_service +from . import pms_service_line diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 3efc32ed72..15c31c6b59 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -18,6 +18,7 @@ class PmsReservationShortInfo(Datamodel): 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) class PmsReservationInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py new file mode 100644 index 0000000000..6102a1bcea --- /dev/null +++ b/pms_api_rest/datamodels/pms_service.py @@ -0,0 +1,14 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsServiceInfo(Datamodel): + _name = "pms.service.info" + id = fields.Integer(required=False, allow_none=True) + name = fields.String(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) 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..5ce638164f --- /dev/null +++ b/pms_api_rest/datamodels/pms_service_line.py @@ -0,0 +1,14 @@ +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) + isBoardService = fields.Boolean(required=False, allow_none=True) + productId = fields.Integer(required=False,allow_none=True) + date = fields.String(required=False, allow_none=True) + priceUnit = fields.Float(required=False, allow_none=True) + priceTotal = fields.Float(required=False, allow_none=True) + discount = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 78d071844a..26be7c9707 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -29,3 +29,4 @@ from . import pms_cancelation_rule_service from . import pms_agency_service +from . import pms_service_line_service diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index a2c70a575b..6edb69e39b 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -254,6 +254,11 @@ def get_folio_reservations(self, folio_id): allowedCheckout=reservation.allowed_checkout, isSplitted=reservation.splitted, priceTotal=reservation.price_room_services_set, + servicesCount=len( + reservation.service_ids.filtered( + lambda x: not x.is_board_service + ) + ), ) ) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index b170ad51c3..bb3c9fbe84 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -249,6 +249,38 @@ def get_reservation_line(self, reservation_id): ) return result_lines + @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: + result_services.append( + PmsServiceInfo( + id=service.id, + name=service.name, + quantity=service.product_qty, + priceTotal=service.price_total, + priceSubtotal=service.price_subtotal, + priceTaxes=service.price_tax, + discount=service.discount, + ) + ) + return result_services + @restapi.method( [ ( 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..7637a00449 --- /dev/null +++ b/pms_api_rest/services/pms_service_line_service.py @@ -0,0 +1,52 @@ +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 PmsServiceService(Component): + _inherit = "base.rest.service" + _name = "pms.reservation.line.service" + _usage = "service" + _collection = "pms.services" + + @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, + isBoardService=service_line.is_board_service, + productId=service_line.product_id.id, + date=datetime.combine( + service_line.date, datetime.min.time() + ).isoformat(), + priceUnit=service_line.price_unit, + priceTotal=service_line.price_day_total, + discount=service_line.discount, + ) + ) + return result_service_lines + From cc0b90b3bf523fec2f6b476339fd28875c8fb647 Mon Sep 17 00:00:00 2001 From: Sara Date: Mon, 11 Jul 2022 17:02:27 +0200 Subject: [PATCH 124/547] [RFC]pms_api_rest: refactor in datamodels and services --- pms_api_rest/datamodels/__init__.py | 2 +- .../datamodels/pms_account_journal.py | 2 +- pms_api_rest/datamodels/pms_agency.py | 1 + pms_api_rest/datamodels/pms_amenity.py | 3 +- .../datamodels/pms_board_service_line.py | 2 +- pms_api_rest/datamodels/pms_folio.py | 3 +- ...ms_id_categories.py => pms_id_category.py} | 4 +- pms_api_rest/datamodels/pms_payment.py | 1 - pms_api_rest/datamodels/pms_pricelist.py | 2 + pms_api_rest/datamodels/pms_property.py | 1 - pms_api_rest/datamodels/pms_reservation.py | 15 ++-- pms_api_rest/datamodels/pms_service_line.py | 2 +- pms_api_rest/services/__init__.py | 4 +- pms_api_rest/services/pms_agency_service.py | 6 +- pms_api_rest/services/pms_amenity_service.py | 6 +- .../services/pms_availability_plan_service.py | 6 +- .../pms_board_service_line_service.py | 6 +- .../services/pms_board_service_service.py | 2 +- pms_api_rest/services/pms_calendar_service.py | 2 +- .../services/pms_cancelation_rule_service.py | 2 +- ...ds_service.py => pms_extra_bed_service.py} | 2 +- pms_api_rest/services/pms_folio_service.py | 64 ++++++++------ ..._service.py => pms_id_category_service.py} | 12 +-- pms_api_rest/services/pms_partner_service.py | 32 +++---- .../services/pms_pricelist_service.py | 4 + pms_api_rest/services/pms_property_service.py | 6 +- .../services/pms_reservation_service.py | 84 +++++++++---------- pms_api_rest/services/pms_room_service.py | 4 +- .../services/pms_room_type_class_service.py | 2 +- .../services/pms_sale_channel_service.py | 6 +- .../services/pms_service_line_service.py | 7 +- .../services/pms_ubication_service.py | 10 +-- pms_api_rest/services/res_city_zip_service.py | 6 +- pms_api_rest/services/res_country_service.py | 2 +- .../services/res_partner_category_service.py | 4 +- 35 files changed, 171 insertions(+), 146 deletions(-) rename pms_api_rest/datamodels/{pms_id_categories.py => pms_id_category.py} (73%) rename pms_api_rest/services/{pms_extra_beds_service.py => pms_extra_bed_service.py} (98%) rename pms_api_rest/services/{pms_id_categories_service.py => pms_id_category_service.py} (71%) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index ddf81cb59d..6482e3d0d7 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -23,7 +23,7 @@ from . import pms_availability_plan from . import pms_availability_plan_rule -from . import pms_id_categories +from . import pms_id_category from . import res_country from . import res_partner_category from . import res_city_zip diff --git a/pms_api_rest/datamodels/pms_account_journal.py b/pms_api_rest/datamodels/pms_account_journal.py index 48527e3464..6c96239bd2 100644 --- a/pms_api_rest/datamodels/pms_account_journal.py +++ b/pms_api_rest/datamodels/pms_account_journal.py @@ -7,4 +7,4 @@ class PmsAccountJournalInfo(Datamodel): _name = "pms.account.journal.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - allowedPmsPayments = fields.Boolean(required=False, allow_none=True) + allowedPayments = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_agency.py b/pms_api_rest/datamodels/pms_agency.py index 045a992ce1..bd4de19e9f 100644 --- a/pms_api_rest/datamodels/pms_agency.py +++ b/pms_api_rest/datamodels/pms_agency.py @@ -12,3 +12,4 @@ class PmsAgencyInfo(Datamodel): _name = "pms.agency.info" id = fields.Integer(required=True, allow_none=False) name = fields.String(required=True, allow_none=False) + image = 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 index b9c970c39a..f52cfceba5 100644 --- a/pms_api_rest/datamodels/pms_amenity.py +++ b/pms_api_rest/datamodels/pms_amenity.py @@ -13,4 +13,5 @@ class PmsAmenityInfo(Datamodel): _name = "pms.amenity.info" id = fields.Integer(required=True, allow_none=False) name = fields.String(required=True, allow_none=False) - amenityTypeId = fields.Integer(required=True, allow_none=False) + defaultCode = fields.String(required=False, allow_none=True) + amenityTypeId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_board_service_line.py b/pms_api_rest/datamodels/pms_board_service_line.py index 1820f8a239..3f271454ac 100644 --- a/pms_api_rest/datamodels/pms_board_service_line.py +++ b/pms_api_rest/datamodels/pms_board_service_line.py @@ -5,7 +5,7 @@ class PmsBoardServiceLineSearchParam(Datamodel): _name = "pms.board.service.line.search.param" - pmsBoarServiceId = fields.Integer(required=True, allow_none=False) + boardServiceId = fields.Integer(required=True, allow_none=False) class PmsBoardServiceLineInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 11b9b07f06..91462e5f8b 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -30,5 +30,6 @@ class PmsFolioShortInfo(Datamodel): partnerPhone = fields.String(required=False, allow_none=True) partnerEmail = fields.String(required=False, allow_none=True) amountTotal = fields.Float(required=False, allow_none=True) - paymentState = fields.String(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)) diff --git a/pms_api_rest/datamodels/pms_id_categories.py b/pms_api_rest/datamodels/pms_id_category.py similarity index 73% rename from pms_api_rest/datamodels/pms_id_categories.py rename to pms_api_rest/datamodels/pms_id_category.py index 09dec38c1a..a802071f0a 100644 --- a/pms_api_rest/datamodels/pms_id_categories.py +++ b/pms_api_rest/datamodels/pms_id_category.py @@ -3,7 +3,7 @@ from odoo.addons.datamodel.core import Datamodel -class PmsIdCategoriesInfo(Datamodel): - _name = "pms.id.categories.info" +class PmsIdCategoryInfo(Datamodel): + _name = "pms.id.category.info" id = fields.Integer(required=False, allow_none=True) documentType = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_payment.py b/pms_api_rest/datamodels/pms_payment.py index 63d34602bd..728870847d 100644 --- a/pms_api_rest/datamodels/pms_payment.py +++ b/pms_api_rest/datamodels/pms_payment.py @@ -8,5 +8,4 @@ class PmsPaymentInfo(Datamodel): id = fields.Integer(required=False, allow_none=True) amount = fields.Float(required=False, allow_none=True) journalId = fields.Integer(required=False, allow_none=True) - journalName = fields.String(required=False, allow_none=True) date = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_pricelist.py b/pms_api_rest/datamodels/pms_pricelist.py index cf10e57fa7..0fd0554daf 100644 --- a/pms_api_rest/datamodels/pms_pricelist.py +++ b/pms_api_rest/datamodels/pms_pricelist.py @@ -7,4 +7,6 @@ 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)) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index 40e4d45509..0e2a7c6bfc 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -14,7 +14,6 @@ class PmsPropertyInfo(Datamodel): name = fields.String(required=False, allow_none=True) company = fields.String(required=False, allow_none=True) defaultPricelistId = fields.Integer(required=False, allow_none=True) - defaultAvailabilityPlanId = 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) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 15c31c6b59..5619b087f7 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -6,17 +6,18 @@ class PmsReservationShortInfo(Datamodel): _name = "pms.reservation.short.info" id = fields.Integer(required=False, allow_none=True) - boardServiceName = fields.String(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) - roomTypeName = fields.String(required=False, allow_none=True) - preferredRoomShortName = fields.String(required=False, allow_none=True) + roomTypeId = fields.Integer(required=False, allow_none=True) + preferredRoomId = fields.Integer(required=False, allow_none=True) adults = fields.Integer(required=False, allow_none=True) - state = 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) allowedCheckout = fields.Boolean(required=False, allow_none=True) - isSplitted = fields.Boolean(required=False, allow_none=True) + splitted = fields.Boolean(required=False, allow_none=True) priceTotal = fields.Float(required=False, allow_none=True) servicesCount = fields.Integer(required=False, allow_none=True) @@ -26,6 +27,7 @@ class PmsReservationInfo(Datamodel): 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) partnerName = fields.String(required=False, allow_none=True) boardServiceId = fields.Integer(required=False, allow_none=True) saleChannelId = fields.Integer(required=False, allow_none=True) @@ -42,7 +44,8 @@ class PmsReservationInfo(Datamodel): adults = fields.Integer(required=False, allow_none=True) overbooking = fields.Boolean(required=False, allow_none=True) externalReference = fields.String(required=False, allow_none=True) - state = 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) allowedCheckout = fields.Boolean(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 index 5ce638164f..a03e868f84 100644 --- a/pms_api_rest/datamodels/pms_service_line.py +++ b/pms_api_rest/datamodels/pms_service_line.py @@ -7,7 +7,7 @@ class PmsServiceLineInfo(Datamodel): _name = "pms.service.line.info" id = fields.Integer(required=False, allow_none=True) isBoardService = fields.Boolean(required=False, allow_none=True) - productId = fields.Integer(required=False,allow_none=True) + productId = fields.Integer(required=False, allow_none=True) date = fields.String(required=False, allow_none=True) priceUnit = fields.Float(required=False, allow_none=True) priceTotal = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 26be7c9707..0ccf5d27d5 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -10,13 +10,13 @@ from . import pms_login_service from . import pms_pricelist_service from . import pms_availability_plan_service -from . import pms_id_categories_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_beds_service +from . import pms_extra_bed_service from . import pms_amenity_service from . import pms_amenity_type_service diff --git a/pms_api_rest/services/pms_agency_service.py b/pms_api_rest/services/pms_agency_service.py index 52298eb8ec..9d33cd95e6 100644 --- a/pms_api_rest/services/pms_agency_service.py +++ b/pms_api_rest/services/pms_agency_service.py @@ -39,6 +39,9 @@ def get_agencies(self, agencies_search_param): PmsAgencyInfo( id=agency.id, name=agency.name, + image=agency.image_1024.decode("utf-8") + if agency.image_1024 + else None, ) ) return result_agencies @@ -66,7 +69,8 @@ def get_agency(self, agency_id): PmsAgencieInfo = self.env.datamodels["pms.agency.info"] return PmsAgencieInfo( id=agency.id, - name=agency.name, + name=agency.name if agency.name else None, + image=agency.image_1024.decode("utf-8") if agency.image_1024 else None, ) 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 index 395100cf05..486c249d77 100644 --- a/pms_api_rest/services/pms_amenity_service.py +++ b/pms_api_rest/services/pms_amenity_service.py @@ -72,8 +72,10 @@ def get_amenity(self, amenity_id): return PmsAmenityInfo( id=amenity.id, name=amenity.name, - defaultCode=amenity.default_code, - pmsAmenityTypeId=amenity.pms_amenity_type_id.id, + 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_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index e51a64043b..8ee6077a6c 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -86,7 +86,9 @@ def get_availability_plan_rules( ) if not record_availability_plan_id: raise MissingError - PmsAvailabilityPlanInfo = self.env.datamodels["pms.availability.plan.rule.info"] + PmsAvailabilityPlanRuleInfo = self.env.datamodels[ + "pms.availability.plan.rule.info" + ] rooms = self.env["pms.room"].search( [ ( @@ -121,7 +123,7 @@ def get_availability_plan_rules( ] ) if rule: - availability_plan_rule_info = PmsAvailabilityPlanInfo( + availability_plan_rule_info = PmsAvailabilityPlanRuleInfo( roomTypeId=room_type.id, date=datetime.combine(date, datetime.min.time()).isoformat(), availabilityRuleId=rule.id, diff --git a/pms_api_rest/services/pms_board_service_line_service.py b/pms_api_rest/services/pms_board_service_line_service.py index 7ae8cc8413..4a9e8a6897 100644 --- a/pms_api_rest/services/pms_board_service_line_service.py +++ b/pms_api_rest/services/pms_board_service_line_service.py @@ -9,7 +9,7 @@ class PmsBoardServiceService(Component): _inherit = "base.rest.service" _name = "pms.board.service.line.service" - _usage = "board-service-line" + _usage = "board-service-lines" _collection = "pms.services" @restapi.method( @@ -27,12 +27,12 @@ class PmsBoardServiceService(Component): ) def get_board_service_lines(self, board_service_lines_search_param): domain = [] - if board_service_lines_search_param.pmsBoardServiceId: + if board_service_lines_search_param.boardServiceId: domain.append( ( "pms_board_service_room_type_id", "=", - board_service_lines_search_param.pmsBoardServiceId, + board_service_lines_search_param.boardServiceId, ) ) diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index ba66ec40c4..94fd1a5277 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -9,7 +9,7 @@ class PmsBoardServiceService(Component): _inherit = "base.rest.service" _name = "pms.board.service.service" - _usage = "board-service" + _usage = "board-services" _collection = "pms.services" @restapi.method( diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index c0014d6274..9d5dce148c 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -7,7 +7,7 @@ class PmsCalendarService(Component): _inherit = "base.rest.service" - _name = "pms.private.services" + _name = "pms.private.service" _usage = "calendar" _collection = "pms.services" diff --git a/pms_api_rest/services/pms_cancelation_rule_service.py b/pms_api_rest/services/pms_cancelation_rule_service.py index 96842bd422..35c15bbf3f 100644 --- a/pms_api_rest/services/pms_cancelation_rule_service.py +++ b/pms_api_rest/services/pms_cancelation_rule_service.py @@ -9,7 +9,7 @@ class PmsCancelationRuleService(Component): _inherit = "base.rest.service" _name = "pms.cancelation.rule.service" - _usage = "cancelation-rule" + _usage = "cancelation-rules" _collection = "pms.services" @restapi.method( diff --git a/pms_api_rest/services/pms_extra_beds_service.py b/pms_api_rest/services/pms_extra_bed_service.py similarity index 98% rename from pms_api_rest/services/pms_extra_beds_service.py rename to pms_api_rest/services/pms_extra_bed_service.py index f9ad835363..2d80863b64 100644 --- a/pms_api_rest/services/pms_extra_beds_service.py +++ b/pms_api_rest/services/pms_extra_bed_service.py @@ -3,7 +3,7 @@ from odoo.addons.component.core import Component -class PmsExtraBedsService(Component): +class PmsExtraBedService(Component): _inherit = "base.rest.service" _name = "pms.extra.beds.service" _usage = "extra-beds" diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 6edb69e39b..bb49f9dca8 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -123,20 +123,23 @@ def get_folios(self, folio_search_param): "checkout": datetime.combine( reservation.checkout, datetime.min.time() ).isoformat(), - "preferredRoomShortName": reservation.preferred_room_id.short_name + "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, "adults": reservation.adults, - "pricelistName": reservation.pricelist_id.name, - "saleChannel": reservation.channel_type_id.name + "pricelistId": reservation.pricelist_id.id + if reservation.pricelist_id + else None, + "saleChannelId": reservation.channel_type_id.id if reservation.channel_type_id else None, - "agency": reservation.agency_id.name + "agencyId": reservation.agency_id.id if reservation.agency_id else None, - "agencyImage": reservation.agency_id.image_1024.decode("utf-8") - if reservation.agency_id and reservation.agency_id.image_1024 - else None, + "splitted": reservation.splitted, } ) result_folios.append( @@ -147,13 +150,12 @@ def get_folios(self, folio_search_param): partnerEmail=folio.email if folio.email else None, amountTotal=folio.amount_total, reservations=[] if not reservations else reservations, - paymentState=dict( + paymentStateCode=folio.payment_state, + paymentStateDescription=dict( folio.fields_get(["payment_state"])["payment_state"][ "selection" ] - )[folio.payment_state] - if folio.payment_state - else None, + )[folio.payment_state], ) ) return result_folios @@ -174,7 +176,8 @@ def get_folios(self, folio_search_param): def get_folio_payments(self, folio_id, pms_search_param): domain = list() domain.append(("id", "=", folio_id)) - domain.append(("pms_property_id", "=", pms_search_param.pmsPropertyId)) + if pms_search_param.pmsPropertyId: + domain.append(("pms_property_id", "=", pms_search_param.pmsPropertyId)) folio = self.env["pms.folio"].search(domain) payments = [] PmsPaymentInfo = self.env.datamodels["pms.payment.info"] @@ -191,9 +194,10 @@ def get_folio_payments(self, folio_id, pms_search_param): PmsPaymentInfo( id=payment.id, amount=payment.amount, - journalId=payment.journal_id, - journalName=payment.journal_id.name, - date=str(payment.date), + journalId=payment.journal_id.id, + date=datetime.combine( + payment.date, datetime.min.time() + ).isoformat(), ) ) if folio.payment_ids: @@ -203,9 +207,10 @@ def get_folio_payments(self, folio_id, pms_search_param): PmsPaymentInfo( id=payment.id, amount=payment.amount, - journalId=payment.journal_id, - journalName=payment.journal_id.name, - date=str(payment.date), + journalId=payment.journal_id.id, + date=datetime.combine( + payment.date, datetime.min.time() + ).isoformat(), ) ) return payments @@ -234,25 +239,32 @@ def get_folio_reservations(self, folio_id): reservations.append( PmsReservationShortInfo( id=reservation.id, - boardServiceName=reservation.board_service_room_id.pms_board_service_id.name - or None, + 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(), - roomTypeName=reservation.room_type_id.name or None, - preferredRoomShortName=reservation.preferred_room_id.short_name - or None, + 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, adults=reservation.adults, - state=dict( + stateCode=reservation.state, + stateDescription=dict( reservation.fields_get(["state"])["state"]["selection"] )[reservation.state], - children=reservation.children or None, + children=reservation.children + if reservation.children + else None, readyForCheckin=reservation.ready_for_checkin, allowedCheckout=reservation.allowed_checkout, - isSplitted=reservation.splitted, + splitted=reservation.splitted, priceTotal=reservation.price_room_services_set, servicesCount=len( reservation.service_ids.filtered( diff --git a/pms_api_rest/services/pms_id_categories_service.py b/pms_api_rest/services/pms_id_category_service.py similarity index 71% rename from pms_api_rest/services/pms_id_categories_service.py rename to pms_api_rest/services/pms_id_category_service.py index 1a7a13e150..57db6a43b8 100644 --- a/pms_api_rest/services/pms_id_categories_service.py +++ b/pms_api_rest/services/pms_id_category_service.py @@ -3,10 +3,10 @@ from odoo.addons.component.core import Component -class PmsIdCategoriesService(Component): +class PmsIdCategoryService(Component): _inherit = "base.rest.service" - _name = "pms.id.categories.services" - _usage = "id_categories" + _name = "pms.id.category.service" + _usage = "id-categories" _collection = "pms.services" @restapi.method( @@ -18,15 +18,15 @@ class PmsIdCategoriesService(Component): "GET", ) ], - output_param=Datamodel("pms.id.categories.info", is_list=True), + output_param=Datamodel("pms.id.category.info", is_list=True), auth="jwt_api_pms", ) def get_id_categories(self): result_id_categories = [] - PmsIdCategoriesInfo = self.env.datamodels["pms.id.categories.info"] + PmsIdCategoryInfo = self.env.datamodels["pms.id.category.info"] for id_category in self.env["res.partner.id_category"].search([]): result_id_categories.append( - PmsIdCategoriesInfo( + PmsIdCategoryInfo( id=id_category.id, documentType=id_category.name, ) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 2a95e6af07..5933c7a682 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -32,7 +32,7 @@ def get_partners(self): result_partners.append( PmsPartnerInfo( id=partner.id, - name=partner.name, + name=partner.name if partner.name else None, ) ) return result_partners @@ -72,51 +72,51 @@ def get_partner_by_doc_number(self, document_type, document_number): # id=doc_number.partner_id.id, name=doc_number.partner_id.name if doc_number.partner_id.name - else "", + else None, firstname=doc_number.partner_id.firstname if doc_number.partner_id.firstname - else "", + else None, lastname=doc_number.partner_id.lastname if doc_number.partner_id.lastname - else "", + else None, lastname2=doc_number.partner_id.lastname2 if doc_number.partner_id.lastname2 - else "", + else None, email=doc_number.partner_id.email if doc_number.partner_id.email - else "", + else None, mobile=doc_number.partner_id.mobile if doc_number.partner_id.mobile - else "", + else None, documentType=doc_type.id, documentNumber=doc_number.name, documentExpeditionDate=document_expedition_date if doc_number.valid_from - else "", + else None, documentSupportNumber=doc_number.support_number if doc_number.support_number - else "", + else None, gender=doc_number.partner_id.gender if doc_number.partner_id.gender - else "", + else None, birthdate=birthdate_date if doc_number.partner_id.birthdate_date - else "", + else None, residenceStreet=doc_number.partner_id.residence_street if doc_number.partner_id.residence_street - else "", + else None, zip=doc_number.partner_id.residence_zip if doc_number.partner_id.residence_zip - else "", + else None, residenceCity=doc_number.partner_id.residence_city if doc_number.partner_id.residence_city - else "", + else None, nationality=doc_number.partner_id.nationality_id.id if doc_number.partner_id.nationality_id - else -1, + else None, countryState=doc_number.partner_id.residence_state_id.id if doc_number.partner_id.residence_state_id - else -1, + else None, ) ) return partners diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 6949f0e0ed..cd5ea779c6 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -60,6 +60,10 @@ def get_pricelists(self, pms_search_param, **args): PmsPricelistInfo( id=pricelist.id, name=pricelist.name, + cancellationPolicyId=pricelist.cancelation_rule_id.id + if pricelist.cancelation_rule_id + else None, + defaultAvailabilityPlanId=pricelist.availability_plan_id.id, pmsPropertyIds=pricelist.pms_property_ids.mapped("id"), ) ) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 991eaaed08..8c6f881e05 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -32,9 +32,7 @@ def get_properties(self): PmsPropertyInfo( id=prop.id, name=prop.name, - company=prop.company_id.name, defaultPricelistId=prop.default_pricelist_id.id, - defaultAvailabilityPlanId=prop.default_pricelist_id.availability_plan_id.id, colorOptionConfig=prop.color_option_config, preReservationColor=prop.pre_reservation_color, confirmedReservationColor=prop.confirmed_reservation_color, @@ -66,7 +64,6 @@ def get_properties(self): ) def get_property(self, property_id): pms_property = self.env["pms.property"].search([("id", "=", property_id)]) - default_avail = pms_property.default_pricelist_id.availability_plan_id.id res = [] PmsPropertyInfo = self.env.datamodels["pms.property.info"] if not pms_property: @@ -77,7 +74,6 @@ def get_property(self, property_id): name=pms_property.name, company=pms_property.company_id.name, defaultPricelistId=pms_property.default_pricelist_id.id, - defaultAvailabilityPlanId=default_avail, colorOptionConfig=pms_property.color_option_config, preReservationColor=pms_property.pre_reservation_color, confirmedReservationColor=pms_property.confirmed_reservation_color, @@ -120,7 +116,7 @@ def get_method_payments_property(self, property_id): PmsAccountJournalInfo( id=payment_method.id, name=payment_method.name, - allowedPmsPayments=payment_method.allowed_pms_payments, + allowedPayments=payment_method.allowed_pms_payments, ) ) return res diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index bb3c9fbe84..fc03566150 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -38,20 +38,6 @@ def get_reservation(self, reservation_id, pms_search_param): if not reservation: pass else: - # services = [] - # for service in reservation.service_ids: - # if service.is_board_service: - # services.append( - # { - # "id": service.id, - # "name": service.name, - # "quantity": service.product_qty, - # "priceTotal": service.price_total, - # "priceSubtotal": service.price_subtotal, - # "priceTaxes": service.price_tax, - # "discount": service.discount, - # } - # ) # messages = [] # import re @@ -69,10 +55,15 @@ def get_reservation(self, reservation_id, pms_search_param): id=reservation.id, name=reservation.name, folioId=reservation.folio_id.id, + folioSequence=reservation.folio_sequence, partnerName=reservation.partner_name, - boardServiceId=reservation.board_service_room_id.id or None, - saleChannelId=reservation.channel_type_id.id or None, - agencyId=reservation.agency_id.id or None, + boardServiceId=reservation.board_service_room_id.id + if reservation.board_service_room_id + else None, + saleChannelId=reservation.channel_type_id.id + if reservation.channel_type_id + else None, + agencyId=reservation.agency_id.id if reservation.agency_id else None, checkin=datetime.combine( reservation.checkin, datetime.min.time() ).isoformat(), @@ -81,16 +72,25 @@ def get_reservation(self, reservation_id, pms_search_param): ).isoformat(), arrivalHour=reservation.arrival_hour, departureHour=reservation.departure_hour, - roomTypeId=reservation.room_type_id.id or None, - preferredRoomId=reservation.preferred_room_id.id or None, - pricelistId=reservation.pricelist_id.id, - adults=reservation.adults, + 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 or None, - state=dict(reservation.fields_get(["state"])["state"]["selection"])[ - reservation.state - ], - children=reservation.children or None, + 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, @@ -101,13 +101,13 @@ def get_reservation(self, reservation_id, pms_search_param): segmentationId=reservation.segmentation_ids[0].id if reservation.segmentation_ids else None, - cancellationPolicyId=reservation.pricelist_id.cancelation_rule_id.id - or None, toAssign=reservation.to_assign, reservationType=reservation.reservation_type, priceTotal=reservation.price_room_services_set, discount=reservation.discount, - commissionAmount=reservation.commission_amount or None, + commissionAmount=reservation.commission_amount + if reservation.commission_amount + else None, priceOnlyServices=reservation.price_services, priceOnlyRoom=reservation.price_total, ) @@ -338,46 +338,46 @@ def get_checkin_partners(self, reservation_id): name=checkin_partner.name if checkin_partner.name else "", firstname=checkin_partner.firstname if checkin_partner.firstname - else "", + else None, lastname=checkin_partner.lastname if checkin_partner.lastname - else "", + else None, lastname2=checkin_partner.lastname2 if checkin_partner.lastname2 - else "", + 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 -1, + else None, documentNumber=checkin_partner.document_number if checkin_partner.document_number - else "", + else None, documentExpeditionDate=document_expedition_date if checkin_partner.document_expedition_date - else "", + else None, documentSupportNumber=checkin_partner.support_number if checkin_partner.support_number - else "", + else None, gender=checkin_partner.gender if checkin_partner.gender else "", birthdate=birthdate_date if checkin_partner.birthdate_date - else "", + else None, residenceStreet=checkin_partner.residence_street if checkin_partner.residence_street - else "", + else None, zip=checkin_partner.residence_zip if checkin_partner.residence_zip - else "", + else None, residenceCity=checkin_partner.residence_city if checkin_partner.residence_city - else "", + else None, nationality=checkin_partner.residence_country_id.id if checkin_partner.residence_country_id - else -1, + else None, countryState=checkin_partner.residence_state_id.id if checkin_partner.residence_state_id - else -1, + else None, checkinPartnerState=checkin_partner.state, ) ) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 793a4bc459..4237541f54 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -78,7 +78,9 @@ def get_rooms(self, room_search_param): roomTypeClassId=room.room_type_id.class_id, ubicationId=room.ubication_id, extraBedsAllowed=room.extra_beds_allowed, - roomAmenityIds=room.room_amenity_ids.ids, + roomAmenityIds=room.room_amenity_ids.ids + if room.room_amenity_ids + else None, ) ) return result_rooms diff --git a/pms_api_rest/services/pms_room_type_class_service.py b/pms_api_rest/services/pms_room_type_class_service.py index f767b65826..876ac23f93 100644 --- a/pms_api_rest/services/pms_room_type_class_service.py +++ b/pms_api_rest/services/pms_room_type_class_service.py @@ -6,7 +6,7 @@ class PmsRoomTypeClassService(Component): _inherit = "base.rest.service" _name = "pms.room.type.class.service" - _usage = "room-type-class" + _usage = "room-type-classes" _collection = "pms.services" @restapi.method( diff --git a/pms_api_rest/services/pms_sale_channel_service.py b/pms_api_rest/services/pms_sale_channel_service.py index 73ba38b184..16f774e93d 100644 --- a/pms_api_rest/services/pms_sale_channel_service.py +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -9,7 +9,7 @@ class PmsSaleChannelService(Component): _inherit = "base.rest.service" _name = "pms.sale.channel.service" - _usage = "sale-channel" + _usage = "sale-channels" _collection = "pms.services" @restapi.method( @@ -58,7 +58,7 @@ def get_sale_channels(self, sale_channel_search_param): result_sale_channels.append( PmsSaleChannelInfo( id=sale_channel.id, - name=sale_channel.name, + name=sale_channel.name if sale_channel.name else None, ) ) return result_sale_channels @@ -83,7 +83,7 @@ def get_sale_channel(self, sale_channel_id): PmsSaleChannelInfo = self.env.datamodels["pms.sale.channel.info"] return PmsSaleChannelInfo( id=sale_channel.id, - name=sale_channel.name, + 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 index 7637a00449..75039d397d 100644 --- a/pms_api_rest/services/pms_service_line_service.py +++ b/pms_api_rest/services/pms_service_line_service.py @@ -11,7 +11,7 @@ class PmsServiceService(Component): _inherit = "base.rest.service" _name = "pms.reservation.line.service" - _usage = "service" + _usage = "services" _collection = "pms.services" @restapi.method( @@ -27,9 +27,7 @@ class PmsServiceService(Component): auth="jwt_api_pms", ) def get_service_lines(self, service_id): - service = self.env["pms.service"].search( - [("id", "=", service_id)] - ) + service = self.env["pms.service"].search([("id", "=", service_id)]) if not service: raise MissingError(_("Service not found")) result_service_lines = [] @@ -49,4 +47,3 @@ def get_service_lines(self, service_id): ) ) return result_service_lines - diff --git a/pms_api_rest/services/pms_ubication_service.py b/pms_api_rest/services/pms_ubication_service.py index a2fe085114..124dd30e40 100644 --- a/pms_api_rest/services/pms_ubication_service.py +++ b/pms_api_rest/services/pms_ubication_service.py @@ -6,7 +6,7 @@ class PmsUbicationService(Component): _inherit = "base.rest.service" _name = "pms.ubication.service" - _usage = "ubication" + _usage = "ubications" _collection = "pms.services" @restapi.method( @@ -47,15 +47,15 @@ def get_ubications(self, ubication_search_param): result_ubications = [] PmsUbicationInfo = self.env.datamodels["pms.ubication.info"] - for room in self.env["pms.ubication"].search( + for ubication in self.env["pms.ubication"].search( domain, ): result_ubications.append( PmsUbicationInfo( - id=room.id, - name=room.name, - pmsPropertyIds=room.pms_property_ids.mapped("id"), + id=ubication.id, + name=ubication.name, + pmsPropertyIds=ubication.pms_property_ids.mapped("id"), ) ) return result_ubications diff --git a/pms_api_rest/services/res_city_zip_service.py b/pms_api_rest/services/res_city_zip_service.py index 12efe97be1..fa35ab4944 100644 --- a/pms_api_rest/services/res_city_zip_service.py +++ b/pms_api_rest/services/res_city_zip_service.py @@ -3,10 +3,10 @@ from odoo.addons.component.core import Component -class PmsIdCategoriesService(Component): +class ResCityZipService(Component): _inherit = "base.rest.service" - _name = "res.city.zip.services" - _usage = "zip" + _name = "res.city.zip.service" + _usage = "zips" _collection = "pms.services" @restapi.method( diff --git a/pms_api_rest/services/res_country_service.py b/pms_api_rest/services/res_country_service.py index e5ff5e0e93..d8bb1c2eea 100644 --- a/pms_api_rest/services/res_country_service.py +++ b/pms_api_rest/services/res_country_service.py @@ -5,7 +5,7 @@ class ResCountryService(Component): _inherit = "base.rest.service" - _name = "res.country.services" + _name = "res.country.service" _usage = "countries" _collection = "pms.services" diff --git a/pms_api_rest/services/res_partner_category_service.py b/pms_api_rest/services/res_partner_category_service.py index 8b99521fd8..b24dfdf1a7 100644 --- a/pms_api_rest/services/res_partner_category_service.py +++ b/pms_api_rest/services/res_partner_category_service.py @@ -3,9 +3,9 @@ from odoo.addons.component.core import Component -class PmsIdCategoriesService(Component): +class PmsPartnerCategoriesService(Component): _inherit = "base.rest.service" - _name = "res.partner.category.services" + _name = "res.partner.category.service" _usage = "segmentations" _collection = "pms.services" From 338eb4321c18aa30ebfcbf73f049dd8a9a14ed1a Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 12 Jul 2022 10:35:44 +0200 Subject: [PATCH 125/547] [FIX] pms-api-rest: num. services, amenities, round(discount), cancelation rule. --- pms_api_rest/datamodels/pms_reservation.py | 2 +- pms_api_rest/datamodels/pms_room.py | 2 +- pms_api_rest/services/pms_folio_service.py | 5 ++++- pms_api_rest/services/pms_pricelist_service.py | 6 ++++-- pms_api_rest/services/pms_reservation_service.py | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 5619b087f7..1cc8a51cb9 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -53,7 +53,7 @@ class PmsReservationInfo(Datamodel): pendingCheckinData = fields.Integer(required=False, allow_none=True) createDate = fields.String(required=False, allow_none=True) segmentationId = fields.Integer(required=False, allow_none=True) - cancellationPolicyId = fields.Integer(required=False, allow_none=True) + cancelationRuleId = fields.Integer(required=False, allow_none=True) toAssign = fields.Boolean(required=False, allow_none=True) reservationType = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index da9eb54575..daff5d12d3 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -23,4 +23,4 @@ class PmsRoomInfo(Datamodel): 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) + roomAmenityIds = fields.List(fields.Integer(), required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index bb49f9dca8..7374f806b2 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -266,10 +266,13 @@ def get_folio_reservations(self, folio_id): allowedCheckout=reservation.allowed_checkout, splitted=reservation.splitted, priceTotal=reservation.price_room_services_set, - servicesCount=len( + # TODO: REVIEW IF THIS OR QTY OF EACH ONE + servicesCount=sum( reservation.service_ids.filtered( + lambda x: not x.is_board_service ) + .mapped("product_qty") ), ) ) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index cd5ea779c6..ee855d9dee 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -60,10 +60,12 @@ def get_pricelists(self, pms_search_param, **args): PmsPricelistInfo( id=pricelist.id, name=pricelist.name, - cancellationPolicyId=pricelist.cancelation_rule_id.id + cancelationRuleId=pricelist.cancelation_rule_id.id if pricelist.cancelation_rule_id else None, - defaultAvailabilityPlanId=pricelist.availability_plan_id.id, + defaultAvailabilityPlanId=pricelist.availability_plan_id.id + if pricelist.availability_plan_id else None + , pmsPropertyIds=pricelist.pms_property_ids.mapped("id"), ) ) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index fc03566150..ee10fb3a18 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -105,7 +105,7 @@ def get_reservation(self, reservation_id, pms_search_param): reservationType=reservation.reservation_type, priceTotal=reservation.price_room_services_set, discount=reservation.discount, - commissionAmount=reservation.commission_amount + commissionAmount=round(reservation.commission_amount, 2) if reservation.commission_amount else None, priceOnlyServices=reservation.price_services, From c3a450f622bd43d6a6d5af9d1addaf28d7f13171 Mon Sep 17 00:00:00 2001 From: Sara Date: Tue, 12 Jul 2022 11:04:06 +0200 Subject: [PATCH 126/547] [RFC]pms_api_rest: round floats and correct output params --- pms_api_rest/datamodels/pms_reservation.py | 1 - .../pms_board_service_line_service.py | 4 ++-- .../services/pms_board_service_service.py | 6 ++--- pms_api_rest/services/pms_calendar_service.py | 8 +++---- pms_api_rest/services/pms_folio_service.py | 13 +++++------ pms_api_rest/services/pms_partner_service.py | 2 +- pms_api_rest/services/pms_product_service.py | 4 ++-- .../services/pms_reservation_line_service.py | 12 +++++----- .../services/pms_reservation_service.py | 22 +++++++++---------- .../services/pms_room_type_service.py | 4 ++-- .../services/pms_service_line_service.py | 6 ++--- 11 files changed, 40 insertions(+), 42 deletions(-) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 1cc8a51cb9..c463e739f8 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -64,5 +64,4 @@ class PmsReservationInfo(Datamodel): priceOnlyRoom = fields.Float(required=False, allow_none=True) # TODO: Refact - # services = fields.List(fields.Dict(required=False, allow_none=True)) # messages = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/pms_board_service_line_service.py b/pms_api_rest/services/pms_board_service_line_service.py index 4a9e8a6897..1c63833298 100644 --- a/pms_api_rest/services/pms_board_service_line_service.py +++ b/pms_api_rest/services/pms_board_service_line_service.py @@ -47,7 +47,7 @@ def get_board_service_lines(self, board_service_lines_search_param): 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=line.amount, + amount=round(line.amount,2), ) ) return result_board_service_lines @@ -75,7 +75,7 @@ def get_board_service_line(self, 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=board_service_line.amount, + 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 index 94fd1a5277..9da9dcf044 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -56,7 +56,7 @@ def get_board_services(self, board_services_search_param): id=board_service.id, name=board_service.pms_board_service_id.display_name, roomTypeId=board_service.pms_room_type_id.id, - amount=board_service.amount, + amount=round(board_service.amount,2), ) ) return result_board_services @@ -83,7 +83,7 @@ def get_board_service(self, board_service_id): id=board_service.id, name=board_service.pms_board_service_id.display_name, roomTypeId=board_service.pms_room_type_id.id, - amount=board_service.amount, + amount=round(board_service.amount), ) else: raise MissingError(_("Board Service not found")) @@ -127,7 +127,7 @@ def get_board_service_lines(self, board_service_id, pms_search_param): 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=line.amount, + amount=round(line.amount,2), ) ) return result_board_service_lines diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 9d5dce148c..1cf26da534 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -73,8 +73,8 @@ def get_calendar(self, calendar_search_param): isFirstNight=line.reservation_id.checkin == line.date, isLastNight=line.reservation_id.checkout + timedelta(days=-1) == line.date, - totalPrice=line.reservation_id.price_total, - pendingPayment=line.reservation_id.folio_pending_amount, + totalPrice=round(line.reservation_id.price_total,2), + pendingPayment=round(line.reservation_id.folio_pending_amount,2), numNotifications=line.reservation_id.message_needaction_counter, adults=line.reservation_id.adults, nextLineSplitted=next_line_splitted, @@ -176,8 +176,8 @@ def get_daily_invoincing(self, pms_calendar_search_param): result.append( PmsCalendarDailyInvoicing( date=datetime.combine(day, datetime.min.time()).isoformat(), - invoicingTotal=sum(reservation_lines_by_day.mapped("price")) - + sum(service_lines_by_day.mapped("price_day_total")), + invoicingTotal=round(sum(reservation_lines_by_day.mapped("price")) + + sum(service_lines_by_day.mapped("price_day_total")),2) ) ) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 7374f806b2..f57efff0ff 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -44,7 +44,7 @@ def get_folio(self, folio_id): state=dict(folio.fields_get(["state"])["state"]["selection"])[ folio.state ], - amountTotal=folio.amount_total, + amountTotal=round(folio.amount_total,2), ) else: raise MissingError(_("Folio not found")) @@ -148,7 +148,7 @@ def get_folios(self, folio_search_param): 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=folio.amount_total, + amountTotal=round(folio.amount_total,2), reservations=[] if not reservations else reservations, paymentStateCode=folio.payment_state, paymentStateDescription=dict( @@ -193,7 +193,7 @@ def get_folio_payments(self, folio_id, pms_search_param): payments.append( PmsPaymentInfo( id=payment.id, - amount=payment.amount, + amount=round(payment.amount,2), journalId=payment.journal_id.id, date=datetime.combine( payment.date, datetime.min.time() @@ -206,7 +206,7 @@ def get_folio_payments(self, folio_id, pms_search_param): payments.append( PmsPaymentInfo( id=payment.id, - amount=payment.amount, + amount=round(payment.amount,2), journalId=payment.journal_id.id, date=datetime.combine( payment.date, datetime.min.time() @@ -224,7 +224,7 @@ def get_folio_payments(self, folio_id, pms_search_param): "GET", ) ], - output_param=Datamodel("pms.reservation.info", is_list=True), + output_param=Datamodel("pms.reservation.short.info", is_list=True), auth="jwt_api_pms", ) def get_folio_reservations(self, folio_id): @@ -265,8 +265,7 @@ def get_folio_reservations(self, folio_id): readyForCheckin=reservation.ready_for_checkin, allowedCheckout=reservation.allowed_checkout, splitted=reservation.splitted, - priceTotal=reservation.price_room_services_set, - # TODO: REVIEW IF THIS OR QTY OF EACH ONE + priceTotal=round(reservation.price_room_services_set,2), servicesCount=sum( reservation.service_ids.filtered( diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 5933c7a682..8153507364 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -46,7 +46,7 @@ def get_partners(self): "GET", ) ], - output_param=Datamodel("pms.partner.info", is_list=True), + output_param=Datamodel("pms.checkin.partner.info", is_list=True), auth="jwt_api_pms", ) def get_partner_by_doc_number(self, document_type, document_number): diff --git a/pms_api_rest/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py index 7372189288..0b8c1db8c2 100644 --- a/pms_api_rest/services/pms_product_service.py +++ b/pms_api_rest/services/pms_product_service.py @@ -52,7 +52,7 @@ def get_products(self, product_search_param): PmsProductInfo( id=product.id, name=product.name, - price=self._get_product_price(product, product_search_param), + price=round(self._get_product_price(product, product_search_param),2), perDay=product.per_day, perPerson=product.per_person, ) @@ -79,7 +79,7 @@ def get_product(self, product_id, product_search_param): return PmsProductInfo( id=product.id, name=product.name, - price=self._get_product_price(product, product_search_param), + price=round(self._get_product_price(product, product_search_param),2), perDay=product.per_day, perPerson=product.per_person, ) diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index 22c2983eed..49adc31fa9 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -37,9 +37,9 @@ def get_reservation_line(self, reservation_line_id): date=datetime.combine( reservation_line.date, datetime.min.time() ).isoformat(), - price=reservation_line.price, - discount=reservation_line.discount, - cancelDiscount=reservation_line.cancel_discount, + 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, @@ -90,9 +90,9 @@ def get_reservation_lines(self, reservation_lines_search_param): date=datetime.combine( reservation_line.date, datetime.min.time() ).isoformat(), - price=reservation_line.price, - discount=reservation_line.discount, - cancelDiscount=reservation_line.cancel_discount, + 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, diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index ee10fb3a18..3c04bc4ae0 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -103,13 +103,13 @@ def get_reservation(self, reservation_id, pms_search_param): else None, toAssign=reservation.to_assign, reservationType=reservation.reservation_type, - priceTotal=reservation.price_room_services_set, - discount=reservation.discount, + 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=reservation.price_services, - priceOnlyRoom=reservation.price_total, + priceOnlyServices=round(reservation.price_services,2), + priceOnlyRoom=round(reservation.price_total,2), ) return res @@ -239,9 +239,9 @@ def get_reservation_line(self, reservation_id): date=datetime.combine( reservation_line.date, datetime.min.time() ).isoformat(), - price=reservation_line.price, - discount=reservation_line.discount, - cancelDiscount=reservation_line.cancel_discount, + 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, @@ -273,10 +273,10 @@ def get_reservation_services(self, reservation_id): id=service.id, name=service.name, quantity=service.product_qty, - priceTotal=service.price_total, - priceSubtotal=service.price_subtotal, - priceTaxes=service.price_tax, - discount=service.discount, + priceTotal=round(service.price_total,2), + priceSubtotal=round(service.price_subtotal,2), + priceTaxes=round(service.price_tax,2), + discount=round(service.discount,2), ) ) return result_services diff --git a/pms_api_rest/services/pms_room_type_service.py b/pms_api_rest/services/pms_room_type_service.py index 8fef9c3608..b852dc21ec 100644 --- a/pms_api_rest/services/pms_room_type_service.py +++ b/pms_api_rest/services/pms_room_type_service.py @@ -19,7 +19,7 @@ class PmsRoomTypeService(Component): ) ], input_param=Datamodel("pms.room.type.search.param"), - output_param=Datamodel("pms.room.info", is_list=True), + output_param=Datamodel("pms.room.type.info", is_list=True), auth="jwt_api_pms", ) def get_room_types(self, room_type_search_param): @@ -59,7 +59,7 @@ def get_room_types(self, room_type_search_param): name=room.name, pmsPropertyIds=room.pms_property_ids.mapped("id"), defaultCode=room.default_code, - price=room.list_price, + price=round(room.list_price,2), ) ) return result_rooms diff --git a/pms_api_rest/services/pms_service_line_service.py b/pms_api_rest/services/pms_service_line_service.py index 75039d397d..288939a687 100644 --- a/pms_api_rest/services/pms_service_line_service.py +++ b/pms_api_rest/services/pms_service_line_service.py @@ -41,9 +41,9 @@ def get_service_lines(self, service_id): date=datetime.combine( service_line.date, datetime.min.time() ).isoformat(), - priceUnit=service_line.price_unit, - priceTotal=service_line.price_day_total, - discount=service_line.discount, + priceUnit=round(service_line.price_unit,2), + priceTotal=round(service_line.price_day_total,2), + discount=round(service_line.discount,2), ) ) return result_service_lines From e10a8733a6a97a291cb38d5c00b4401461178aaf Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 13 Jul 2022 19:48:04 +0200 Subject: [PATCH 127/547] [IMP]: added GET for res_users and is_board_service field in pms_service --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/datamodels/pms_service.py | 1 + pms_api_rest/datamodels/res_users.py | 9 +++++++ .../pms_board_service_line_service.py | 4 ++-- .../services/pms_board_service_service.py | 4 ++-- pms_api_rest/services/pms_calendar_service.py | 11 +++++---- pms_api_rest/services/pms_folio_service.py | 14 +++++------ .../services/pms_pricelist_service.py | 4 ++-- pms_api_rest/services/pms_product_service.py | 6 +++-- pms_api_rest/services/pms_property_service.py | 22 +++++++++++++++++ .../services/pms_reservation_line_service.py | 12 +++++----- .../services/pms_reservation_service.py | 24 ++++++++++--------- .../services/pms_room_type_service.py | 2 +- .../services/pms_service_line_service.py | 6 ++--- 15 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 pms_api_rest/datamodels/res_users.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 6482e3d0d7..10c6a60c96 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -44,3 +44,4 @@ from . import pms_agency from . import pms_service from . import pms_service_line +from . import res_users diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index c463e739f8..ace95c09f3 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -32,6 +32,7 @@ class PmsReservationInfo(Datamodel): boardServiceId = fields.Integer(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) diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py index 6102a1bcea..0afda50d85 100644 --- a/pms_api_rest/datamodels/pms_service.py +++ b/pms_api_rest/datamodels/pms_service.py @@ -12,3 +12,4 @@ class PmsServiceInfo(Datamodel): 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) diff --git a/pms_api_rest/datamodels/res_users.py b/pms_api_rest/datamodels/res_users.py new file mode 100644 index 0000000000..8ba270427c --- /dev/null +++ b/pms_api_rest/datamodels/res_users.py @@ -0,0 +1,9 @@ +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) diff --git a/pms_api_rest/services/pms_board_service_line_service.py b/pms_api_rest/services/pms_board_service_line_service.py index 1c63833298..5aeba45ada 100644 --- a/pms_api_rest/services/pms_board_service_line_service.py +++ b/pms_api_rest/services/pms_board_service_line_service.py @@ -47,7 +47,7 @@ def get_board_service_lines(self, board_service_lines_search_param): 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), + amount=round(line.amount, 2), ) ) return result_board_service_lines @@ -75,7 +75,7 @@ def get_board_service_line(self, 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), + 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 index 9da9dcf044..4281077c50 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -56,7 +56,7 @@ def get_board_services(self, board_services_search_param): 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), + amount=round(board_service.amount, 2), ) ) return result_board_services @@ -127,7 +127,7 @@ def get_board_service_lines(self, board_service_id, pms_search_param): 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), + amount=round(line.amount, 2), ) ) return result_board_service_lines diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 1cf26da534..4903322b93 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -73,8 +73,8 @@ def get_calendar(self, calendar_search_param): isFirstNight=line.reservation_id.checkin == line.date, isLastNight=line.reservation_id.checkout + timedelta(days=-1) == line.date, - totalPrice=round(line.reservation_id.price_total,2), - pendingPayment=round(line.reservation_id.folio_pending_amount,2), + totalPrice=round(line.reservation_id.price_total, 2), + pendingPayment=round(line.reservation_id.folio_pending_amount, 2), numNotifications=line.reservation_id.message_needaction_counter, adults=line.reservation_id.adults, nextLineSplitted=next_line_splitted, @@ -176,8 +176,11 @@ def get_daily_invoincing(self, pms_calendar_search_param): result.append( PmsCalendarDailyInvoicing( date=datetime.combine(day, datetime.min.time()).isoformat(), - invoicingTotal=round(sum(reservation_lines_by_day.mapped("price")) - + sum(service_lines_by_day.mapped("price_day_total")),2) + invoicingTotal=round( + sum(reservation_lines_by_day.mapped("price")) + + sum(service_lines_by_day.mapped("price_day_total")), + 2, + ), ) ) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index f57efff0ff..51691056c9 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -44,7 +44,7 @@ def get_folio(self, folio_id): state=dict(folio.fields_get(["state"])["state"]["selection"])[ folio.state ], - amountTotal=round(folio.amount_total,2), + amountTotal=round(folio.amount_total, 2), ) else: raise MissingError(_("Folio not found")) @@ -148,7 +148,7 @@ def get_folios(self, folio_search_param): 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), + amountTotal=round(folio.amount_total, 2), reservations=[] if not reservations else reservations, paymentStateCode=folio.payment_state, paymentStateDescription=dict( @@ -193,7 +193,7 @@ def get_folio_payments(self, folio_id, pms_search_param): payments.append( PmsPaymentInfo( id=payment.id, - amount=round(payment.amount,2), + amount=round(payment.amount, 2), journalId=payment.journal_id.id, date=datetime.combine( payment.date, datetime.min.time() @@ -206,7 +206,7 @@ def get_folio_payments(self, folio_id, pms_search_param): payments.append( PmsPaymentInfo( id=payment.id, - amount=round(payment.amount,2), + amount=round(payment.amount, 2), journalId=payment.journal_id.id, date=datetime.combine( payment.date, datetime.min.time() @@ -265,13 +265,11 @@ def get_folio_reservations(self, folio_id): readyForCheckin=reservation.ready_for_checkin, allowedCheckout=reservation.allowed_checkout, splitted=reservation.splitted, - priceTotal=round(reservation.price_room_services_set,2), + priceTotal=round(reservation.price_room_services_set, 2), servicesCount=sum( reservation.service_ids.filtered( - lambda x: not x.is_board_service - ) - .mapped("product_qty") + ).mapped("product_qty") ), ) ) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index ee855d9dee..4a5efa9fc7 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -64,8 +64,8 @@ def get_pricelists(self, pms_search_param, **args): if pricelist.cancelation_rule_id else None, defaultAvailabilityPlanId=pricelist.availability_plan_id.id - if pricelist.availability_plan_id else None - , + if pricelist.availability_plan_id + else None, pmsPropertyIds=pricelist.pms_property_ids.mapped("id"), ) ) diff --git a/pms_api_rest/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py index 0b8c1db8c2..f2a7610837 100644 --- a/pms_api_rest/services/pms_product_service.py +++ b/pms_api_rest/services/pms_product_service.py @@ -52,7 +52,9 @@ def get_products(self, product_search_param): PmsProductInfo( id=product.id, name=product.name, - price=round(self._get_product_price(product, product_search_param),2), + price=round( + self._get_product_price(product, product_search_param), 2 + ), perDay=product.per_day, perPerson=product.per_person, ) @@ -79,7 +81,7 @@ def get_product(self, product_id, product_search_param): return PmsProductInfo( id=product.id, name=product.name, - price=round(self._get_product_price(product, product_search_param),2), + price=round(self._get_product_price(product, product_search_param), 2), perDay=product.per_day, perPerson=product.per_person, ) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 8c6f881e05..624d8feddf 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -120,3 +120,25 @@ def get_method_payments_property(self, property_id): ) ) 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_id", "=", pms_property_id)] + ) + for user in users: + result_users.append(ResUsersInfo(id=user.id, name=user.name)) + return result_users diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index 49adc31fa9..ac05932c36 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -37,9 +37,9 @@ def get_reservation_line(self, 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), + 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, @@ -90,9 +90,9 @@ def get_reservation_lines(self, reservation_lines_search_param): 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), + 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, diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 3c04bc4ae0..139a1502a8 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -64,6 +64,7 @@ def get_reservation(self, reservation_id, pms_search_param): if reservation.channel_type_id else None, agencyId=reservation.agency_id.id if reservation.agency_id else None, + userId=reservation.user_id.id, checkin=datetime.combine( reservation.checkin, datetime.min.time() ).isoformat(), @@ -103,13 +104,13 @@ def get_reservation(self, reservation_id, pms_search_param): else None, toAssign=reservation.to_assign, reservationType=reservation.reservation_type, - priceTotal=round(reservation.price_room_services_set,2), - discount=round(reservation.discount,2), + 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), + priceOnlyServices=round(reservation.price_services, 2), + priceOnlyRoom=round(reservation.price_total, 2), ) return res @@ -239,9 +240,9 @@ def get_reservation_line(self, reservation_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), + 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, @@ -273,10 +274,11 @@ def get_reservation_services(self, reservation_id): id=service.id, name=service.name, 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), + 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, ) ) return result_services diff --git a/pms_api_rest/services/pms_room_type_service.py b/pms_api_rest/services/pms_room_type_service.py index b852dc21ec..4615fa1a79 100644 --- a/pms_api_rest/services/pms_room_type_service.py +++ b/pms_api_rest/services/pms_room_type_service.py @@ -59,7 +59,7 @@ def get_room_types(self, room_type_search_param): name=room.name, pmsPropertyIds=room.pms_property_ids.mapped("id"), defaultCode=room.default_code, - price=round(room.list_price,2), + price=round(room.list_price, 2), ) ) return result_rooms diff --git a/pms_api_rest/services/pms_service_line_service.py b/pms_api_rest/services/pms_service_line_service.py index 288939a687..7a6708d1b0 100644 --- a/pms_api_rest/services/pms_service_line_service.py +++ b/pms_api_rest/services/pms_service_line_service.py @@ -41,9 +41,9 @@ def get_service_lines(self, service_id): date=datetime.combine( service_line.date, datetime.min.time() ).isoformat(), - priceUnit=round(service_line.price_unit,2), - priceTotal=round(service_line.price_day_total,2), - discount=round(service_line.discount,2), + priceUnit=round(service_line.price_unit, 2), + priceTotal=round(service_line.price_day_total, 2), + discount=round(service_line.discount, 2), ) ) return result_service_lines From 007b224bbad54d8fe6ddf4d5840dfeb0a385bc0a Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 13 Jul 2022 20:57:35 +0200 Subject: [PATCH 128/547] [IMP]pms-api-rest: added reservationType in folio.info datamodel --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 91462e5f8b..53bba30a87 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -21,6 +21,7 @@ class PmsFolioInfo(Datamodel): 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) class PmsFolioShortInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 51691056c9..df5d246595 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -45,6 +45,7 @@ def get_folio(self, folio_id): folio.state ], amountTotal=round(folio.amount_total, 2), + reservationType=folio.reservation_type, ) else: raise MissingError(_("Folio not found")) From 942dcca227dd5d5aae9230a4666a5bdcbaf23c80 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 14 Jul 2022 11:13:41 +0200 Subject: [PATCH 129/547] [FIX]pms-api-rest: fixed service of property users and create date field in reservation service --- pms_api_rest/services/pms_property_service.py | 2 +- pms_api_rest/services/pms_reservation_service.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 624d8feddf..d889e3237c 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -137,7 +137,7 @@ def get_users(self, pms_property_id): result_users = [] ResUsersInfo = self.env.datamodels["res.users.info"] users = self.env["res.users"].search( - [("pms_property_id", "=", pms_property_id)] + [("pms_property_ids", "in", pms_property_id)] ) for user in users: result_users.append(ResUsersInfo(id=user.id, name=user.name)) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 139a1502a8..37b038e46d 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -96,9 +96,7 @@ def get_reservation(self, reservation_id, pms_search_param): allowedCheckout=reservation.allowed_checkout, isSplitted=reservation.splitted, pendingCheckinData=reservation.pending_checkin_data, - createDate=datetime.combine( - reservation.create_date, datetime.min.time() - ).isoformat(), + createDate=reservation.create_date.isoformat(), segmentationId=reservation.segmentation_ids[0].id if reservation.segmentation_ids else None, From e44310d48007935c5b5ad597bf8b64b6b75c94d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 13 Jul 2022 17:03:11 +0200 Subject: [PATCH 130/547] [FIX]pms_api_rest: Add DELETE reservation line service --- .../services/pms_reservation_line_service.py | 2 +- .../services/pms_reservation_service.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index ac05932c36..411f419002 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -10,7 +10,7 @@ class PmsReservationLineService(Component): _inherit = "base.rest.service" - _name = "pms.reservation.line.service" + _name = "pms.reservationline.service" _usage = "reservation-lines" _collection = "pms.services" diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 37b038e46d..c37348086f 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -248,6 +248,30 @@ def get_reservation_line(self, reservation_id): ) return result_lines + @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 len(reservation.reservation_line_ids) == 1 + and line.date > min(reservation.reservation_line_ids.mapped("date")) + and 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( [ ( From 05f5f2761b52149e0c6a8c48fc77cfb1a545694a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 13 Jul 2022 18:00:24 +0200 Subject: [PATCH 131/547] [FIX]pms_api_rest: Add pms prices services and datamodels --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_price.py | 21 +++++ pms_api_rest/datamodels/pms_product.py | 5 -- pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/pms_price_service.py | 86 ++++++++++++++++++++ pms_api_rest/services/pms_product_service.py | 31 ------- 6 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_price.py create mode 100644 pms_api_rest/services/pms_price_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 10c6a60c96..bf04d41602 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -20,6 +20,7 @@ from . import pms_pricelist from . import pms_pricelist_item +from . import pms_price from . import pms_availability_plan from . import pms_availability_plan_rule diff --git a/pms_api_rest/datamodels/pms_price.py b/pms_api_rest/datamodels/pms_price.py new file mode 100644 index 0000000000..08aa8a5c15 --- /dev/null +++ b/pms_api_rest/datamodels/pms_price.py @@ -0,0 +1,21 @@ +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) + productId = fields.Integer(required=False, allow_none=True) + productQty = fields.Integer(required=False, allow_none=True) + partnerId = fields.Integer(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_product.py b/pms_api_rest/datamodels/pms_product.py index 60bd6a202e..a404700092 100644 --- a/pms_api_rest/datamodels/pms_product.py +++ b/pms_api_rest/datamodels/pms_product.py @@ -7,10 +7,6 @@ class PmsProductSearchParam(Datamodel): _name = "pms.product.search.param" name = fields.String(required=False, allow_none=True) pmsPropertyId = fields.Integer(required=True, allow_none=False) - pricelistId = fields.Integer(required=True, allow_none=False) - partnerId = fields.Integer(required=False, allow_none=True) - dateConsumption = fields.String(required=False, allow_none=True) - productQty = fields.Integer(required=False, allow_none=True) class PmProductInfo(Datamodel): @@ -19,4 +15,3 @@ class PmProductInfo(Datamodel): name = fields.String(required=False, allow_none=True) perDay = fields.Boolean(required=False, allow_none=True) perPerson = fields.Boolean(required=False, allow_none=True) - price = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 0ccf5d27d5..f5ff7c26ea 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -9,6 +9,7 @@ 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 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..717d813bb4 --- /dev/null +++ b/pms_api_rest/services/pms_price_service.py @@ -0,0 +1,86 @@ +from odoo import _, fields +from odoo.exceptions import MissingError +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 + + +class PmsAgencyService(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 = False + if prices_search_param.productId: + product = self.env["product.product"].search([("id", "=", prices_search_param.productId)]) + if prices_search_param.roomTypeId: + room_type = self.env["pms.room.type"].search([("id", "=", prices_search_param.roomTypeId)]) + if (product and room_type) or (not product and not room_type): + raise MissingError(_("It is necessary to indicate one and only one product or room type")) + product = product if product else room_type.product_id + + 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: + result_prices.append( + PmsPriceInfo( + date=datetime.combine( + price_date, datetime.min.time() + ).isoformat(), + price=round( + self._get_product_price(product, prices_search_param, price_date), 2 + ), + ) + ) + return result_prices + + def _get_product_price(self, product, price_search_param, date_consumption=False): + pms_property = self.env["pms.property"].browse( + price_search_param.pmsPropertyId + ) + product_context = dict( + self.env.context, + date=datetime.today().date(), + pricelist=price_search_param.pricelistId or False, + uom=product.uom_id.id, + fiscal_position=False, + property=price_search_param.pmsPropertyId, + ) + if date_consumption: + product_context["consumption_date"] = date_consumption + 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=price_search_param.pricelistId, + product=product, + company_id=pms_property.company_id.id, + product_qty=price_search_param.productQty or 1, + partner_id=price_search_param.partnerId or False, + ), + product.taxes_id, + product.taxes_id, # Not exist service line, we repeat product taxes + pms_property.company_id, + ) diff --git a/pms_api_rest/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py index f2a7610837..42259cfa6f 100644 --- a/pms_api_rest/services/pms_product_service.py +++ b/pms_api_rest/services/pms_product_service.py @@ -52,9 +52,6 @@ def get_products(self, product_search_param): PmsProductInfo( id=product.id, name=product.name, - price=round( - self._get_product_price(product, product_search_param), 2 - ), perDay=product.per_day, perPerson=product.per_person, ) @@ -81,37 +78,9 @@ def get_product(self, product_id, product_search_param): return PmsProductInfo( id=product.id, name=product.name, - price=round(self._get_product_price(product, product_search_param), 2), perDay=product.per_day, perPerson=product.per_person, ) else: raise MissingError(_("Product not found")) - def _get_product_price(self, product, product_search_param): - pms_property = self.env["pms.property"].browse( - product_search_param.pmsPropertyId - ) - product_context = dict( - self.env.context, - date=datetime.today().date(), - pricelist=product_search_param.pricelistId or False, - uom=product.uom_id.id, - fiscal_position=False, - property=product_search_param.pmsPropertyId, - ) - if product_search_param.dateConsumption: - product_context["consumption_date"] = product_search_param.dateConsumption - 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=product_search_param.pricelistId, - product=product, - company_id=pms_property.company_id.id, - product_qty=product_search_param.productQty or 1, - partner_id=product_search_param.partnerId, - ), - product.taxes_id, - product.taxes_id, # Not exist service line, we repeat product taxes - pms_property.company_id, - ) From 85604ec6434608d4c464cc2011239c319b2d1c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 13 Jul 2022 18:46:20 +0200 Subject: [PATCH 132/547] [ADD]pms_api_rest: Add POST reservation//reservation-lines --- .../services/pms_reservation_service.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index c37348086f..53a7cb9dee 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -248,6 +248,41 @@ def get_reservation_line(self, reservation_id): ) 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( [ ( From 58f3e7cdd5d01d24cd982420ab07888205a9c0bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 14 Jul 2022 18:59:04 +0200 Subject: [PATCH 133/547] [ADD]pms_api_rest: Add Price API Service boardServiceId Option --- pms_api_rest/datamodels/pms_price.py | 1 + pms_api_rest/services/pms_price_service.py | 105 ++++++++++++++++----- 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/pms_api_rest/datamodels/pms_price.py b/pms_api_rest/datamodels/pms_price.py index 08aa8a5c15..a69118cca4 100644 --- a/pms_api_rest/datamodels/pms_price.py +++ b/pms_api_rest/datamodels/pms_price.py @@ -10,6 +10,7 @@ class PmsPriceSearchParam(Datamodel): 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) productId = fields.Integer(required=False, allow_none=True) productQty = fields.Integer(required=False, allow_none=True) partnerId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_price_service.py b/pms_api_rest/services/pms_price_service.py index 717d813bb4..0f829aa0e4 100644 --- a/pms_api_rest/services/pms_price_service.py +++ b/pms_api_rest/services/pms_price_service.py @@ -27,14 +27,17 @@ class PmsAgencyService(Component): auth="jwt_api_pms", ) def get_prices(self, prices_search_param): - product = room_type = False - if prices_search_param.productId: - product = self.env["product.product"].search([("id", "=", prices_search_param.productId)]) + product = room_type = board_service = False if prices_search_param.roomTypeId: room_type = self.env["pms.room.type"].search([("id", "=", prices_search_param.roomTypeId)]) - if (product and room_type) or (not product and not room_type): - raise MissingError(_("It is necessary to indicate one and only one product or room type")) - product = product if product else room_type.product_id + if prices_search_param.productId: + product = self.env["product.product"].search([("id", "=", prices_search_param.productId)]) + if prices_search_param.boardServiceId: + board_service = self.env["pms.board.service.room.type"].search([ + ("id", "=", prices_search_param.boardServiceId)] + ) + if sum([var is not False for var in [product, room_type, board_service]]) != 1: + raise MissingError(_("It is necessary to indicate one and only one product, board service or room type")) PmsPriceInfo = self.env.datamodels["pms.price.info"] result_prices = [] @@ -45,42 +48,100 @@ def get_prices(self, prices_search_param): for x in range(0, (date_to - date_from).days + 1) ] for price_date in dates: - result_prices.append( - PmsPriceInfo( - date=datetime.combine( - price_date, datetime.min.time() - ).isoformat(), - price=round( - self._get_product_price(product, prices_search_param, price_date), 2 - ), + if board_service: + result_prices.append( + PmsPriceInfo( + date=datetime.combine( + price_date, datetime.min.time() + ).isoformat(), + price=round( + self._get_board_service_price( + board_service=board_service, + 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 + ), + ) + ) + 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, price_search_param, date_consumption=False): + 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, + ): pms_property = self.env["pms.property"].browse( - price_search_param.pmsPropertyId + pms_property_id ) product_context = dict( self.env.context, date=datetime.today().date(), - pricelist=price_search_param.pricelistId or False, + pricelist=pricelist_id, uom=product.uom_id.id, fiscal_position=False, - property=price_search_param.pmsPropertyId, + property=pms_property_id, ) if date_consumption: product_context["consumption_date"] = date_consumption + if board_service_id: + product_context["board_service"] = board_service_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=price_search_param.pricelistId, + pricelist_id=pricelist_id, product=product, company_id=pms_property.company_id.id, - product_qty=price_search_param.productQty or 1, - partner_id=price_search_param.partnerId or False, + 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, + pms_property_id, + pricelist_id=False, + partner_id=False, + product_qty=False, + date_consumption=False, + ): + price = 0 + for product in board_service.board_service_line_ids.mapped("product_id"): + price += self._get_product_price( + product=product, + pms_property_id=pms_property_id, + pricelist_id=pricelist_id, + partner_id=partner_id, + product_qty=product_qty, + date_consumption=date_consumption, + ) + return price From 057809d0c77b6993a7271599963a2507b5961b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 14 Jul 2022 20:08:12 +0200 Subject: [PATCH 134/547] [FIX]pms_api_rest: camelCase pmsPropertyId in calendar service --- pms_api_rest/services/pms_calendar_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 4903322b93..c9a0897e01 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -108,7 +108,7 @@ def swap_reservation_slices(self, swap_info): ("room_id", "=", room_id_a), ("date", ">=", swap_info.swapFrom), ("date", "<=", swap_info.swapTo), - ("pms_property_id", "=", swap_info.pms_property_id), + ("pms_property_id", "=", swap_info.pmsPropertyId), ] ) From c8d2469f53365aeb73ed313c7017cfe2fc8fb9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 14 Jul 2022 20:09:30 +0200 Subject: [PATCH 135/547] [IMP]pms_api_rest: Disable GET multi reservation lines by security reasons --- .../services/pms_reservation_line_service.py | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index 411f419002..8133d89353 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -47,58 +47,58 @@ def get_reservation_line(self, reservation_line_id): else: raise MissingError(_("Reservation Line not found")) - @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, - ), - ] - ) + # @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 + # 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( [ From be0e9f0f9cd7da83324a3c049e559280676be45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 14 Jul 2022 20:17:38 +0200 Subject: [PATCH 136/547] [IMP]pms_api_rest: Enable delete last reservation line in reservation one night --- pms_api_rest/services/pms_reservation_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 53a7cb9dee..f69d7b013a 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -299,7 +299,6 @@ def delete_reservation_line(self, reservation_id, reservation_line_id): line = reservation.reservation_line_ids.filtered(lambda l: l.id == reservation_line_id) if ( line - and len(reservation.reservation_line_ids) == 1 and line.date > min(reservation.reservation_line_ids.mapped("date")) and line.date < max(reservation.reservation_line_ids.mapped("date")) ): From 6bb72c72c91bbdf85a5598d216ee403eef2bfc80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 15 Jul 2022 09:38:24 +0200 Subject: [PATCH 137/547] [FIX]pms_api_rest: Naming pms_service_service --- pms_api_rest/services/__init__.py | 2 +- pms_api_rest/services/pms_reservation_line_service.py | 2 +- .../{pms_service_line_service.py => pms_service_service.py} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename pms_api_rest/services/{pms_service_line_service.py => pms_service_service.py} (97%) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index f5ff7c26ea..d23f7d890a 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -30,4 +30,4 @@ from . import pms_cancelation_rule_service from . import pms_agency_service -from . import pms_service_line_service +from . import pms_service_service diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index 8133d89353..47308e62c4 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -10,7 +10,7 @@ class PmsReservationLineService(Component): _inherit = "base.rest.service" - _name = "pms.reservationline.service" + _name = "pms.reservation.line.service" _usage = "reservation-lines" _collection = "pms.services" diff --git a/pms_api_rest/services/pms_service_line_service.py b/pms_api_rest/services/pms_service_service.py similarity index 97% rename from pms_api_rest/services/pms_service_line_service.py rename to pms_api_rest/services/pms_service_service.py index 7a6708d1b0..38ab9a5ece 100644 --- a/pms_api_rest/services/pms_service_line_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -10,7 +10,7 @@ class PmsServiceService(Component): _inherit = "base.rest.service" - _name = "pms.reservation.line.service" + _name = "pms.service.service" _usage = "services" _collection = "pms.services" From 8d9df96c196b5800631fe8589db130f33456b0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 18 Jul 2022 13:04:21 +0200 Subject: [PATCH 138/547] [WIP]pms_api_rest: POST services reservation --- pms_api_rest/datamodels/pms_service.py | 1 + .../services/pms_reservation_service.py | 68 +++++++++++++++---- pms_api_rest/services/pms_service_service.py | 30 ++++++++ 3 files changed, 86 insertions(+), 13 deletions(-) diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py index 0afda50d85..9879e3ddf8 100644 --- a/pms_api_rest/datamodels/pms_service.py +++ b/pms_api_rest/datamodels/pms_service.py @@ -7,6 +7,7 @@ class PmsServiceInfo(Datamodel): _name = "pms.service.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) + productId = fields.Integer(required=True, allow_none=False) quantity = fields.Integer(required=False, allow_none=True) priceTotal = fields.Float(required=False, allow_none=True) priceSubtotal = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index f69d7b013a..593031f182 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -14,6 +14,10 @@ class PmsReservationService(Component): _usage = "reservations" _collection = "pms.services" + # ------------------------------------------------------------------------------------ + # HEAD RESERVATION-------------------------------------------------------------------- + # ------------------------------------------------------------------------------------ + @restapi.method( [ ( @@ -213,6 +217,10 @@ def update_reservation(self, reservation_id, reservation_lines_changes): ) reservation_to_update.write(reservation_vals) + # ------------------------------------------------------------------------------------ + # RESERVATION LINES------------------------------------------------------------------- + # ------------------------------------------------------------------------------------ + @restapi.method( [ ( @@ -299,13 +307,40 @@ def delete_reservation_line(self, reservation_id, reservation_line_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")) - and line.date < max(reservation.reservation_line_ids.mapped("date")) + 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( + [ + ( + [ + "//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( [ ( @@ -329,6 +364,7 @@ def get_reservation_services(self, reservation_id): 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), @@ -343,22 +379,28 @@ def get_reservation_services(self, reservation_id): [ ( [ - "//reservation-lines/", + "//services", ], - "PATCH", + "POST", ) ], - input_param=Datamodel("pms.reservation.line.info", is_list=False), + input_param=Datamodel("pms.service.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 + 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.quantity, + "reservation_id": reservation.id, + } + service = self.env["pms.service"].create(vals) + return service.id + + # ------------------------------------------------------------------------------------ + # RESERVATION CHECKINS---------------------------------------------------------------- + # ------------------------------------------------------------------------------------ @restapi.method( [ diff --git a/pms_api_rest/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py index 38ab9a5ece..7ba5c60d88 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -14,6 +14,36 @@ class PmsServiceService(Component): _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"] + + 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, + ) + @restapi.method( [ ( From 09e16769d623fba9a08c68e89a1cd2e7890f17ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 21 Jul 2022 19:26:21 +0200 Subject: [PATCH 139/547] [RFC]pms_api_rest: Clean fields in service line datamodel --- pms_api_rest/datamodels/pms_service_line.py | 4 +--- pms_api_rest/services/pms_service_service.py | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/datamodels/pms_service_line.py b/pms_api_rest/datamodels/pms_service_line.py index a03e868f84..b1d2425e10 100644 --- a/pms_api_rest/datamodels/pms_service_line.py +++ b/pms_api_rest/datamodels/pms_service_line.py @@ -6,9 +6,7 @@ class PmsServiceLineInfo(Datamodel): _name = "pms.service.line.info" id = fields.Integer(required=False, allow_none=True) - isBoardService = fields.Boolean(required=False, allow_none=True) - productId = fields.Integer(required=False, allow_none=True) date = fields.String(required=False, allow_none=True) priceUnit = fields.Float(required=False, allow_none=True) - priceTotal = 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/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py index 7ba5c60d88..de6d055820 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -42,6 +42,7 @@ def get_service(self, service_id): priceTaxes=round(service.price_tax, 2), discount=round(service.discount, 2), isBoardService=service.is_board_service, + ) @restapi.method( @@ -66,14 +67,12 @@ def get_service_lines(self, service_id): result_service_lines.append( PmsServiceLineInfo( id=service_line.id, - isBoardService=service_line.is_board_service, - productId=service_line.product_id.id, date=datetime.combine( service_line.date, datetime.min.time() ).isoformat(), priceUnit=round(service_line.price_unit, 2), - priceTotal=round(service_line.price_day_total, 2), discount=round(service_line.discount, 2), + quantity=service_line.day_qty, ) ) return result_service_lines From 743889e68603127fe49c224ace350238ded0cae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 22 Jul 2022 09:58:05 +0200 Subject: [PATCH 140/547] [ADD]pms_api_rest: Add serviceLines field like NestedModel in service.service --- pms_api_rest/datamodels/pms_service.py | 2 ++ pms_api_rest/services/pms_reservation_service.py | 11 ++++++++++- pms_api_rest/services/pms_service_service.py | 9 ++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py index 9879e3ddf8..74c73b6cf4 100644 --- a/pms_api_rest/datamodels/pms_service.py +++ b/pms_api_rest/datamodels/pms_service.py @@ -1,6 +1,7 @@ from marshmallow import fields from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel class PmsServiceInfo(Datamodel): @@ -14,3 +15,4 @@ class PmsServiceInfo(Datamodel): 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 = NestedModel("pms.service.line.info", many=True) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 593031f182..11e2eb32c0 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -392,9 +392,17 @@ def create_reservation_service(self, reservation_id, service_info): if not reservation: raise MissingError(_("Reservation not found")) vals = { - "product_id": service_info.quantity, + "product_id": service_info.productId, "reservation_id": reservation.id, + "is_board_service": service_info.isBoardService or False, } + if service_info.serviceLines: + 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"].create(vals) return service.id @@ -608,3 +616,4 @@ def _get_checkin_partner_values(self, pms_checkin_partner_info): ): vals.update({"residence_state_id": pms_checkin_partner_info.countryState}) return vals + diff --git a/pms_api_rest/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py index de6d055820..73730d73dc 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -42,7 +42,14 @@ def get_service(self, service_id): priceTaxes=round(service.price_tax, 2), discount=round(service.discount, 2), isBoardService=service.is_board_service, - + serviceLines=[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, + ) for line in service.service_line_ids], ) @restapi.method( From 068734adfc08b9d966179f26fe63def55da38558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 22 Jul 2022 17:31:24 +0200 Subject: [PATCH 141/547] [ADD]pms_api_rest: Add service-lines services --- pms_api_rest/datamodels/pms_service.py | 2 +- pms_api_rest/services/__init__.py | 1 + .../services/pms_reservation_service.py | 15 +++ .../services/pms_service_line_service.py | 93 +++++++++++++++++++ pms_api_rest/services/pms_service_service.py | 40 ++++++-- 5 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 pms_api_rest/services/pms_service_line_service.py diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py index 74c73b6cf4..4c900b2f4a 100644 --- a/pms_api_rest/datamodels/pms_service.py +++ b/pms_api_rest/datamodels/pms_service.py @@ -15,4 +15,4 @@ class PmsServiceInfo(Datamodel): 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 = NestedModel("pms.service.line.info", many=True) + serviceLines = fields.List(NestedModel("pms.service.line.info")) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index d23f7d890a..b2312c6b71 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -31,3 +31,4 @@ from . import pms_agency_service from . import pms_service_service +from . import pms_service_line_service diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 11e2eb32c0..c54875e678 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -357,9 +357,23 @@ 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, @@ -371,6 +385,7 @@ def get_reservation_services(self, reservation_id): priceTaxes=round(service.price_tax, 2), discount=round(service.discount, 2), isBoardService=service.is_board_service, + serviceLines=service_lines, ) ) return result_services 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..6d5f4ec968 --- /dev/null +++ b/pms_api_rest/services/pms_service_line_service.py @@ -0,0 +1,93 @@ +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( + [ + ( + [ + "/", + ], + "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: + vals["discount"] = pms_service_line_info_data.discount + if pms_service_line_info_data.quantity: + vals["day_qty"] = pms_service_line_info_data.quantity + if pms_service_line_info_data.priceUnit: + 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 index 73730d73dc..97cc58eb42 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -31,7 +31,17 @@ def get_service(self, 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, @@ -42,16 +52,28 @@ def get_service(self, service_id): priceTaxes=round(service.price_tax, 2), discount=round(service.discount, 2), isBoardService=service.is_board_service, - serviceLines=[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, - ) for line in service.service_line_ids], + serviceLines=lines, ) + @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( [ ( From 3ef767897ea946efdbd73c704e1fcaa5b3c73731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 9 Aug 2022 11:45:56 +0200 Subject: [PATCH 142/547] [IMP]pms_api_rest: add product consumed on --- pms_api_rest/datamodels/pms_product.py | 1 + pms_api_rest/services/pms_product_service.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/datamodels/pms_product.py b/pms_api_rest/datamodels/pms_product.py index a404700092..9a11a0bd2d 100644 --- a/pms_api_rest/datamodels/pms_product.py +++ b/pms_api_rest/datamodels/pms_product.py @@ -15,3 +15,4 @@ class PmProductInfo(Datamodel): 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/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py index 42259cfa6f..a45176e6ac 100644 --- a/pms_api_rest/services/pms_product_service.py +++ b/pms_api_rest/services/pms_product_service.py @@ -1,5 +1,3 @@ -from datetime import datetime - from odoo import _ from odoo.exceptions import MissingError @@ -54,6 +52,7 @@ def get_products(self, product_search_param): name=product.name, perDay=product.per_day, perPerson=product.per_person, + consumedOn=product.consumed_on, ) ) return result_products @@ -83,4 +82,3 @@ def get_product(self, product_id, product_search_param): ) else: raise MissingError(_("Product not found")) - From f4d41064de3cd975f04c1500d9fea89bad4ec06f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 9 Aug 2022 12:12:12 +0200 Subject: [PATCH 143/547] [IMP]pms_api_rest: add PATCH service and serviceLines field in pms service --- pms_api_rest/datamodels/pms_service.py | 2 +- pms_api_rest/services/pms_service_service.py | 74 ++++++++++++++++++-- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py index 4c900b2f4a..49ce06f78c 100644 --- a/pms_api_rest/datamodels/pms_service.py +++ b/pms_api_rest/datamodels/pms_service.py @@ -8,7 +8,7 @@ class PmsServiceInfo(Datamodel): _name = "pms.service.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - productId = fields.Integer(required=True, allow_none=False) + 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) diff --git a/pms_api_rest/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py index 97cc58eb42..0e2aa42846 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime from odoo import _ @@ -7,6 +8,8 @@ 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" @@ -34,13 +37,12 @@ def get_service(self, service_id): lines = [ self.env.datamodels["pms.service.line.info"]( id=line.id, - date=datetime.combine( - line.date, datetime.min.time() - ).isoformat(), + 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 + ) + for line in service.service_line_ids ] return PmsServiceInfo( id=service.id, @@ -55,6 +57,70 @@ def get_service(self, service_id): serviceLines=lines, ) + @restapi.method( + [ + ( + [ + "/", + ], + "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 and ( + not service_line or origin_data.priceUnit != service_line.price_unit + ): + line_vals["price_unit"] = origin_data.priceUnit + if origin_data.discount and ( + not service_line or origin_data.discount != service_line.discount + ): + line_vals["discount"] = origin_data.discount + if origin_data.quantity and ( + not service_line or origin_data.quantity != service_line.day_qty + ): + line_vals["day_qty"] = origin_data.quantity + return line_vals + @restapi.method( [ ( From 8778e1f0a4f8b0e420a6614d22c3f944f1721918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 9 Aug 2022 12:12:35 +0200 Subject: [PATCH 144/547] [RFC]pms_api_rest: Refactor PATCH reservation with lines like a NestedModel --- pms_api_rest/datamodels/pms_reservation.py | 3 + pms_api_rest/services/pms_calendar_service.py | 101 ++++++++ .../services/pms_reservation_service.py | 236 +++++++++--------- 3 files changed, 225 insertions(+), 115 deletions(-) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index ace95c09f3..5808b0842c 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -1,6 +1,7 @@ from marshmallow import fields from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel class PmsReservationShortInfo(Datamodel): @@ -64,5 +65,7 @@ class PmsReservationInfo(Datamodel): priceOnlyServices = fields.Float(required=False, allow_none=True) priceOnlyRoom = fields.Float(required=False, allow_none=True) + reservationLines = fields.List(NestedModel("pms.reservation.line.info")) + # TODO: Refact # messages = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index c9a0897e01..8932cd32b8 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -294,3 +294,104 @@ def get_alerts_per_day(self, pms_calendar_search_param): ) ) return result + + @restapi.method( + [ + ( + [ + "/", + ], + "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: + + # 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: + 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: + 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_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index c54875e678..bb8d18b143 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -125,97 +125,92 @@ def get_reservation(self, reservation_id, pms_search_param): "PATCH", ) ], - input_param=Datamodel("pms.reservation.updates", is_list=False), + input_param=Datamodel("pms.reservation.info", is_list=False), auth="jwt_api_pms", ) - def update_reservation(self, reservation_id, reservation_lines_changes): - if reservation_lines_changes.reservationLinesChanges: - - # 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"), + 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"), ): - # recordset of each line - line_to_change = self.env["pms.reservation.line"].search( - [ - ("reservation_id", "=", reservation_id), - ("id", "=", change_iterator["reservationLineId"]), - ] + 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)] ) - # 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: - 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: - reservation_vals.update( - {"children": reservation_lines_changes.children} - ) - if reservation_lines_changes.segmentationId: + 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( { - "segmentation_ids": [ - (6, 0, [reservation_lines_changes.segmentationId]) - ] + "reservation_line_ids": reservation_lines_vals, } ) - reservation_to_update.write(reservation_vals) + + if reservation_data.preferredRoomId: + reservation_vals.update( + {"preferred_room_id": reservation_data.preferredRoomId} + ) + if reservation_data.boardServiceId: + reservation_vals.update( + {"board_service_room_id": reservation_data.boardServiceId} + ) + 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: + reservation_vals.update({"children": reservation_data.children}) + if reservation_data.segmentationId: + reservation_vals.update( + {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} + ) + reservation.write(reservation_vals) + + 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 origin_data.price != reservation_line.price + ): + line_vals["price"] = origin_data.price + if origin_data.discount and ( + not reservation_line or origin_data.discount != reservation_line.discount + ): + line_vals["discount"] = origin_data.discount + if origin_data.cancelDiscount and ( + not reservation_line + or origin_data.cancelDiscount != reservation_line.cancelDiscount + ): + line_vals["cancel_discount"] = origin_data.cancelDiscount + if origin_data.roomId and ( + not reservation_line or origin_data.roomId != reservation_line.room_id + ): + line_vals["room_id"] = origin_data.roomId + return line_vals # ------------------------------------------------------------------------------------ # RESERVATION LINES------------------------------------------------------------------- @@ -279,16 +274,20 @@ def create_reservation_line(self, reservation_id, reservation_line_info): date != reservation.checkin - timedelta(days=1) and date != reservation.checkout ): - raise MissingError(_("It is only allowed to create contiguous nights to the reservation")) + 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, - }) + 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( @@ -304,13 +303,12 @@ def create_reservation_line(self, reservation_id, reservation_line_info): ) 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 = 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: @@ -364,15 +362,17 @@ def get_reservation_services(self, reservation_id): 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, - )) + 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( @@ -412,12 +412,19 @@ def create_reservation_service(self, reservation_id, service_info): "is_board_service": service_info.isBoardService or False, } if service_info.serviceLines: - 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] + 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"].create(vals) return service.id @@ -631,4 +638,3 @@ def _get_checkin_partner_values(self, pms_checkin_partner_info): ): vals.update({"residence_state_id": pms_checkin_partner_info.countryState}) return vals - From f2f22a01bc1addc1b5e4cca54c389c7f2ce9be5b Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 14 Jul 2022 20:32:04 +0200 Subject: [PATCH 145/547] [IMP]pms-api-rest: added configurable avail rules in pms_property --- pms_api_rest/datamodels/pms_property.py | 1 + pms_api_rest/models/pms_property.py | 18 ++++++++++++++++++ .../services/pms_availability_plan_service.py | 15 +++++++-------- pms_api_rest/services/pms_property_service.py | 8 ++++++++ pms_api_rest/views/pms_property_views.xml | 8 ++++++++ 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index 0e2a7c6bfc..ffe7bbae3e 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -27,3 +27,4 @@ class PmsPropertyInfo(Datamodel): simpleOutColor = fields.String(required=False, allow_none=True) simpleInColor = fields.String(required=False, allow_none=True) simpleFutureColor = fields.String(required=False, allow_none=True) + availabilityRuleFields = fields.List(fields.String(), required=False, allow_none=True) diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index a9c224253f..5482dc38ce 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -82,3 +82,21 @@ class PmsProperty(models.Model): help="Color for pending payment reservations in the planning.", default="rgba(162,70,137)", ) + + availability_rule_field_ids = fields.Many2many( + string="Availability Rules", + help="Configurable availability rules", + default=lambda x: x._get_default_avail_rule_fields(), + comodel_name="ir.model.fields", + relation="ir_model_fields_pms_property_rel", + column1="ir_model_fields", + column2="pms_property", + + ) + + def _get_default_avail_rule_fields(self): + avail_rule_fields = self.env['ir.model.fields'].search([('model_id', '=', 'pms.availability.plan.rule'), ('name', 'in', ('min_stay', 'quota'))]) + if avail_rule_fields: + return avail_rule_fields.ids + else: + return [] diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 8ee6077a6c..cc9382038f 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -220,14 +220,13 @@ def write_availability_plan_rule( vals.update( {"max_stay_arrival": pms_avail_plan_rule_info.maxStayArrival} ) - if pms_avail_plan_rule_info.closed: - vals.update({"closed": pms_avail_plan_rule_info.closed}) - if pms_avail_plan_rule_info.closedDeparture: - vals.update( - {"closed_departure": pms_avail_plan_rule_info.closedDeparture} - ) - if pms_avail_plan_rule_info.closedArrival: - vals.update({"closed_arrival": pms_avail_plan_rule_info.closedArrival}) if pms_avail_plan_rule_info.quota: vals.update({"quota": pms_avail_plan_rule_info.quota}) + vals.update( + { + "closed": pms_avail_plan_rule_info.closed, + "closed_departure": pms_avail_plan_rule_info.closedDeparture, + "closed_arrival": pms_avail_plan_rule_info.closedArrival + } + ) avail_rule.write(vals) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index d889e3237c..dcbcc0dee2 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -28,6 +28,9 @@ def get_properties(self): for prop in self.env["pms.property"].search( domain, ): + avail_rule_names = [] + for avail_field in prop.availability_rule_field_ids: + avail_rule_names.append(avail_field.name) result_properties.append( PmsPropertyInfo( id=prop.id, @@ -46,6 +49,7 @@ def get_properties(self): simpleOutColor=prop.simple_out_color, simpleInColor=prop.simple_in_color, simpleFutureColor=prop.simple_future_color, + availabilityRuleFields=avail_rule_names, ) ) return result_properties @@ -65,10 +69,13 @@ def get_properties(self): def get_property(self, property_id): pms_property = self.env["pms.property"].search([("id", "=", property_id)]) res = [] + avail_rule_names = [] PmsPropertyInfo = self.env.datamodels["pms.property.info"] if not pms_property: pass else: + for avail_field in pms_property.availability_rule_field_ids: + avail_rule_names.append(avail_field.name) res = PmsPropertyInfo( id=pms_property.id, name=pms_property.name, @@ -84,6 +91,7 @@ def get_property(self, property_id): staffReservationColor=pms_property.staff_reservation_color, toAssignReservationColor=pms_property.to_assign_reservation_color, pendingPaymentReservationColor=pms_property.pending_payment_reservation_color, + availabilityRuleFields=avail_rule_names ) return res diff --git a/pms_api_rest/views/pms_property_views.xml b/pms_api_rest/views/pms_property_views.xml index cc3afd76e2..8c6c35ec61 100644 --- a/pms_api_rest/views/pms_property_views.xml +++ b/pms_api_rest/views/pms_property_views.xml @@ -71,6 +71,14 @@ attrs="{'invisible': [('color_option_config', '!=', 'advanced')]}" /> + + + From 3dbe3dd189c9cda7a63c43e9c666601c049b4f0d Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 15 Jul 2022 19:03:14 +0200 Subject: [PATCH 146/547] [IMP]:pms-pwa: change configurable avail rule fields from property to user --- pms_api_rest/__manifest__.py | 1 + pms_api_rest/datamodels/pms_property.py | 1 - pms_api_rest/datamodels/pms_user.py | 1 + pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/pms_property.py | 18 --------------- pms_api_rest/models/res_users.py | 23 +++++++++++++++++++ .../services/pms_availability_plan_service.py | 23 +++++-------------- pms_api_rest/services/pms_login_service.py | 4 ++++ pms_api_rest/services/pms_property_service.py | 7 ------ pms_api_rest/views/pms_property_views.xml | 8 ------- pms_api_rest/views/res_users_views.xml | 22 ++++++++++++++++++ 11 files changed, 58 insertions(+), 51 deletions(-) create mode 100644 pms_api_rest/models/res_users.py create mode 100644 pms_api_rest/views/res_users_views.xml diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 90e77815ad..e235886f10 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -20,6 +20,7 @@ "data": [ "data/auth_jwt_validator.xml", "views/pms_property_views.xml", + "views/res_users_views.xml" ], "installable": True, } diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index ffe7bbae3e..0e2a7c6bfc 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -27,4 +27,3 @@ class PmsPropertyInfo(Datamodel): simpleOutColor = fields.String(required=False, allow_none=True) simpleInColor = fields.String(required=False, allow_none=True) simpleFutureColor = fields.String(required=False, allow_none=True) - availabilityRuleFields = fields.List(fields.String(), required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index a3b3900710..2c2dee9670 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -18,3 +18,4 @@ class PmsApiRestUserOutput(Datamodel): userImageBase64 = fields.String(required=False, allow_none=True) defaultPropertyId = fields.Integer(required=True, allow_none=False) defaultPropertyName = fields.String(required=True, allow_none=False) + availabilityRuleFields = fields.List(fields.String(), required=False, allow_none=True) diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 9216f6ffd4..49652c7020 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -1 +1,2 @@ from . import pms_property +from . import res_users diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index 5482dc38ce..a9c224253f 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -82,21 +82,3 @@ class PmsProperty(models.Model): help="Color for pending payment reservations in the planning.", default="rgba(162,70,137)", ) - - availability_rule_field_ids = fields.Many2many( - string="Availability Rules", - help="Configurable availability rules", - default=lambda x: x._get_default_avail_rule_fields(), - comodel_name="ir.model.fields", - relation="ir_model_fields_pms_property_rel", - column1="ir_model_fields", - column2="pms_property", - - ) - - def _get_default_avail_rule_fields(self): - avail_rule_fields = self.env['ir.model.fields'].search([('model_id', '=', 'pms.availability.plan.rule'), ('name', 'in', ('min_stay', 'quota'))]) - if avail_rule_fields: - return avail_rule_fields.ids - else: - return [] diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py new file mode 100644 index 0000000000..cb2c943285 --- /dev/null +++ b/pms_api_rest/models/res_users.py @@ -0,0 +1,23 @@ +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", + default=lambda self: self._get_default_avail_rule_fields(), + comodel_name="ir.model.fields", + relation="ir_model_fields_res_users_rel", + column1="ir_model_fields", + column2="res_users", + + ) + + 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/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index cc9382038f..c5a2c90779 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -134,7 +134,7 @@ def get_availability_plan_rules( closed=rule.closed, closedDeparture=rule.closed_departure, closedArrival=rule.closed_arrival, - quota=rule.quota, + quota=rule.quota if rule.quota != -1 else 0, ) result.append(availability_plan_rule_info) @@ -208,25 +208,14 @@ def write_availability_plan_rule( ] ) if avail_rule: - if pms_avail_plan_rule_info.minStay: - vals.update({"min_stay": pms_avail_plan_rule_info.minStay}) - if pms_avail_plan_rule_info.minStayArrival: - vals.update( - {"min_stay_arrival": pms_avail_plan_rule_info.minStayArrival} - ) - if pms_avail_plan_rule_info.maxStay: - vals.update({"max_stay": pms_avail_plan_rule_info.maxStay}) - if pms_avail_plan_rule_info.maxStayArrival: - vals.update( - {"max_stay_arrival": pms_avail_plan_rule_info.maxStayArrival} - ) - if pms_avail_plan_rule_info.quota: - vals.update({"quota": pms_avail_plan_rule_info.quota}) - vals.update( + avail_rule.write( { + "min_stay": pms_avail_plan_rule_info.minStay, + "max_stay": pms_avail_plan_rule_info.maxStay, + "max_stay_arrival": pms_avail_plan_rule_info.maxStayArrival, + "quota": pms_avail_plan_rule_info.quota, "closed": pms_avail_plan_rule_info.closed, "closed_departure": pms_avail_plan_rule_info.closedDeparture, "closed_arrival": pms_avail_plan_rule_info.closedArrival } ) - avail_rule.write(vals) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 72ea073acd..95f650437d 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -60,6 +60,9 @@ def login(self, user): key="pms_secret_key_example", algorithm=jwt.ALGORITHMS.HS256, ) + avail_rule_names = [] + for avail_field in user_record.availability_rule_field_ids: + avail_rule_names.append(avail_field.name) return PmsApiRestUserOutput( token=token, @@ -69,4 +72,5 @@ def login(self, user): defaultPropertyId=user_record.pms_property_id.id, defaultPropertyName=user_record.pms_property_id.name, userImageBase64=user_record.partner_id.image_1024, + availabilityRuleFields=avail_rule_names, ) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index dcbcc0dee2..0420479414 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -28,9 +28,6 @@ def get_properties(self): for prop in self.env["pms.property"].search( domain, ): - avail_rule_names = [] - for avail_field in prop.availability_rule_field_ids: - avail_rule_names.append(avail_field.name) result_properties.append( PmsPropertyInfo( id=prop.id, @@ -49,7 +46,6 @@ def get_properties(self): simpleOutColor=prop.simple_out_color, simpleInColor=prop.simple_in_color, simpleFutureColor=prop.simple_future_color, - availabilityRuleFields=avail_rule_names, ) ) return result_properties @@ -74,8 +70,6 @@ def get_property(self, property_id): if not pms_property: pass else: - for avail_field in pms_property.availability_rule_field_ids: - avail_rule_names.append(avail_field.name) res = PmsPropertyInfo( id=pms_property.id, name=pms_property.name, @@ -91,7 +85,6 @@ def get_property(self, property_id): staffReservationColor=pms_property.staff_reservation_color, toAssignReservationColor=pms_property.to_assign_reservation_color, pendingPaymentReservationColor=pms_property.pending_payment_reservation_color, - availabilityRuleFields=avail_rule_names ) return res diff --git a/pms_api_rest/views/pms_property_views.xml b/pms_api_rest/views/pms_property_views.xml index 8c6c35ec61..cc3afd76e2 100644 --- a/pms_api_rest/views/pms_property_views.xml +++ b/pms_api_rest/views/pms_property_views.xml @@ -71,14 +71,6 @@ attrs="{'invisible': [('color_option_config', '!=', 'advanced')]}" /> - - - 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..731260c729 --- /dev/null +++ b/pms_api_rest/views/res_users_views.xml @@ -0,0 +1,22 @@ + + + + User Properties fields + res.users + + + + + + + + + + + + From 8a915066199c982036f6a90a227ca531b6317290 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 19 Jul 2022 11:04:20 +0200 Subject: [PATCH 147/547] [FIX]pms_api_rest: fixed default to_assign color in pms_property and changed avail_rule_fields in res users views --- pms_api_rest/models/pms_property.py | 2 +- pms_api_rest/views/res_users_views.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index a9c224253f..80ea18de5e 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -74,7 +74,7 @@ class PmsProperty(models.Model): 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,)", + default="rgba(237,114,46)", ) pending_payment_reservation_color = fields.Char( diff --git a/pms_api_rest/views/res_users_views.xml b/pms_api_rest/views/res_users_views.xml index 731260c729..a51ff887a9 100644 --- a/pms_api_rest/views/res_users_views.xml +++ b/pms_api_rest/views/res_users_views.xml @@ -5,7 +5,7 @@ res.users - + Date: Fri, 22 Jul 2022 14:08:19 +0200 Subject: [PATCH 148/547] [IMP]pms_api_rest: added min stay and quota in demo users --- pms_api_rest/__manifest__.py | 5 ++++- pms_api_rest/datamodels/pms_user.py | 4 +++- pms_api_rest/demo/pms_api_rest_master_data.xml | 17 +++++++++++++++++ pms_api_rest/models/res_users.py | 12 ++++++++---- .../services/pms_availability_plan_service.py | 3 +-- pms_api_rest/services/pms_property_service.py | 1 - pms_api_rest/views/res_users_views.xml | 5 ++++- 7 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 pms_api_rest/demo/pms_api_rest_master_data.xml diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index e235886f10..0664236b56 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -20,7 +20,10 @@ "data": [ "data/auth_jwt_validator.xml", "views/pms_property_views.xml", - "views/res_users_views.xml" + "views/res_users_views.xml", + ], + "demo": [ + "demo/pms_api_rest_master_data.xml", ], "installable": True, } diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index 2c2dee9670..51e01ab3d2 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -18,4 +18,6 @@ class PmsApiRestUserOutput(Datamodel): userImageBase64 = fields.String(required=False, allow_none=True) defaultPropertyId = fields.Integer(required=True, allow_none=False) defaultPropertyName = fields.String(required=True, allow_none=False) - availabilityRuleFields = fields.List(fields.String(), required=False, allow_none=True) + availabilityRuleFields = fields.List( + 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..bca00d7173 --- /dev/null +++ b/pms_api_rest/demo/pms_api_rest_master_data.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index cb2c943285..f6bd3db21a 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -7,16 +7,20 @@ class ResUsers(models.Model): availability_rule_field_ids = fields.Many2many( string="Availability Rules", help="Configurable availability rules", - default=lambda self: self._get_default_avail_rule_fields(), + default=lambda self: self._default_avail_rule_fields(), comodel_name="ir.model.fields", relation="ir_model_fields_res_users_rel", column1="ir_model_fields", column2="res_users", - ) - 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'))]) + def _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: diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index c5a2c90779..bf3292d9ff 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -200,7 +200,6 @@ def create_availability_plan_rule( def write_availability_plan_rule( self, availability_plan_id, availability_plan_rule_id, pms_avail_plan_rule_info ): - vals = dict() avail_rule = self.env["pms.availability.plan.rule"].search( [ ("availability_plan_id", "=", availability_plan_id), @@ -216,6 +215,6 @@ def write_availability_plan_rule( "quota": pms_avail_plan_rule_info.quota, "closed": pms_avail_plan_rule_info.closed, "closed_departure": pms_avail_plan_rule_info.closedDeparture, - "closed_arrival": pms_avail_plan_rule_info.closedArrival + "closed_arrival": pms_avail_plan_rule_info.closedArrival, } ) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 0420479414..d889e3237c 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -65,7 +65,6 @@ def get_properties(self): def get_property(self, property_id): pms_property = self.env["pms.property"].search([("id", "=", property_id)]) res = [] - avail_rule_names = [] PmsPropertyInfo = self.env.datamodels["pms.property.info"] if not pms_property: pass diff --git a/pms_api_rest/views/res_users_views.xml b/pms_api_rest/views/res_users_views.xml index a51ff887a9..9f77bf081b 100644 --- a/pms_api_rest/views/res_users_views.xml +++ b/pms_api_rest/views/res_users_views.xml @@ -5,7 +5,10 @@ res.users - + Date: Wed, 27 Jul 2022 17:47:09 +0200 Subject: [PATCH 149/547] [REF]pms_api_rest: mapping checkin partner values method refactored --- .../services/pms_reservation_service.py | 66 +++++++------------ 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index bb8d18b143..0dea752b16 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -533,7 +533,7 @@ def write_reservation_checkin_partner( ) if checkin_partner: checkin_partner.write( - self._get_checkin_partner_values(pms_checkin_partner_info) + self.mapping_checkin_partner_values(pms_checkin_partner_info) ) @restapi.method( @@ -564,7 +564,7 @@ def create_reservation_checkin_partner( checkin_partner_last_id ) checkin_partner.write( - self._get_checkin_partner_values(pms_checkin_partner_info) + self.mapping_checkin_partner_values(pms_checkin_partner_info) ) @restapi.method( @@ -582,59 +582,37 @@ def delete_reservation_checkin_partner(self, reservation_id, checkin_partner_id) reservation = self.env["pms.reservation"].browse(reservation_id) reservation.adults = reservation.adults - 1 - def _get_checkin_partner_values(self, pms_checkin_partner_info): + def mapping_checkin_partner_values(self, pms_checkin_partner_info): vals = dict() - if pms_checkin_partner_info.firstname: - vals.update({"firstname": pms_checkin_partner_info.firstname}) - if pms_checkin_partner_info.lastname: - vals.update({"lastname": pms_checkin_partner_info.lastname}) - if pms_checkin_partner_info.lastname2: - vals.update({"lastname2": pms_checkin_partner_info.lastname2}) - if pms_checkin_partner_info.email: - vals.update({"email": pms_checkin_partner_info.email}) - if pms_checkin_partner_info.mobile: - vals.update({"mobile": pms_checkin_partner_info.mobile}) - if ( - pms_checkin_partner_info.documentType - and pms_checkin_partner_info.documentType != -1 - ): - document_type = pms_checkin_partner_info.documentType - vals.update({"document_type": document_type}) - if pms_checkin_partner_info.documentNumber: - vals.update({"document_number": pms_checkin_partner_info.documentNumber}) + checkin_partner_fields = { + "firstname": pms_checkin_partner_info.firstname, + "lastname": pms_checkin_partner_info.lastname, + "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, + "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.nationality, + } 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}) - if pms_checkin_partner_info.documentSupportNumber: - vals.update( - {"support_number": pms_checkin_partner_info.documentSupportNumber} - ) - if pms_checkin_partner_info.gender: - vals.update({"gender": pms_checkin_partner_info.gender}) 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}) - if pms_checkin_partner_info.residenceStreet: - vals.update({"residence_street": pms_checkin_partner_info.residenceStreet}) - if pms_checkin_partner_info.zip: - vals.update({"residence_zip": pms_checkin_partner_info.zip}) - if pms_checkin_partner_info.residenceCity: - vals.update({"residence_city": pms_checkin_partner_info.residenceCity}) - if ( - pms_checkin_partner_info.nationality - and pms_checkin_partner_info.nationality != -1 - ): - vals.update({"nationality_id": pms_checkin_partner_info.nationality}) - vals.update({"residence_country_id": pms_checkin_partner_info.nationality}) - if ( - pms_checkin_partner_info.countryState - and pms_checkin_partner_info.countryState != -1 - ): - vals.update({"residence_state_id": pms_checkin_partner_info.countryState}) + for k, v in checkin_partner_fields.items(): + if v: + vals.update({k: v}) return vals From 7c3bed7680fc1106248999b85ed8ab81a23e7b99 Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 8 Aug 2022 11:54:18 +0200 Subject: [PATCH 150/547] [IMP]pms_api_rest: added max avail and min stay arrival in avail rule services, datamodel and res users model --- .../datamodels/pms_availability_plan_rule.py | 1 + pms_api_rest/models/res_users.py | 13 ------ .../services/pms_availability_plan_service.py | 40 ++++++++++++++----- pms_api_rest/views/res_users_views.xml | 2 +- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/pms_api_rest/datamodels/pms_availability_plan_rule.py b/pms_api_rest/datamodels/pms_availability_plan_rule.py index e25373f921..be534ecd42 100644 --- a/pms_api_rest/datamodels/pms_availability_plan_rule.py +++ b/pms_api_rest/datamodels/pms_availability_plan_rule.py @@ -23,4 +23,5 @@ class PmsAvailabilityPlanRuleInfo(Datamodel): 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) diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index f6bd3db21a..f5a02a57c5 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -7,21 +7,8 @@ class ResUsers(models.Model): availability_rule_field_ids = fields.Many2many( string="Availability Rules", help="Configurable availability rules", - default=lambda self: self._default_avail_rule_fields(), comodel_name="ir.model.fields", relation="ir_model_fields_res_users_rel", column1="ir_model_fields", column2="res_users", ) - - def _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/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index bf3292d9ff..59d0f83a12 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -135,6 +135,7 @@ def get_availability_plan_rules( closedDeparture=rule.closed_departure, closedArrival=rule.closed_arrival, quota=rule.quota if rule.quota != -1 else 0, + maxAvailability=rule.max_avail, ) result.append(availability_plan_rule_info) @@ -181,6 +182,8 @@ def create_availability_plan_rule( vals.update({"closed_arrival": pms_avail_plan_rule_info.closedArrival}) if pms_avail_plan_rule_info.quota: vals.update({"quota": pms_avail_plan_rule_info.quota}) + if pms_avail_plan_rule_info.maxAvailability: + vals.update({"max_avail": pms_avail_plan_rule_info.maxAvailability}) avail_plan_rule = self.env["pms.availability.plan.rule"].create(vals) return avail_plan_rule.id @@ -207,14 +210,29 @@ def write_availability_plan_rule( ] ) if avail_rule: - avail_rule.write( - { - "min_stay": pms_avail_plan_rule_info.minStay, - "max_stay": pms_avail_plan_rule_info.maxStay, - "max_stay_arrival": pms_avail_plan_rule_info.maxStayArrival, - "quota": pms_avail_plan_rule_info.quota, - "closed": pms_avail_plan_rule_info.closed, - "closed_departure": pms_avail_plan_rule_info.closedDeparture, - "closed_arrival": pms_avail_plan_rule_info.closedArrival, - } - ) + vals = dict() + if pms_avail_plan_rule_info.minStay: + vals.update({"min_stay": pms_avail_plan_rule_info.minStay}) + if pms_avail_plan_rule_info.minStayArrival: + vals.update( + {"min_stay_arrival": pms_avail_plan_rule_info.minStayArrival} + ) + if pms_avail_plan_rule_info.maxStay: + vals.update({"max_stay": pms_avail_plan_rule_info.maxStay}) + if pms_avail_plan_rule_info.maxStayArrival: + vals.update( + {"max_stay_arrival": pms_avail_plan_rule_info.maxStayArrival} + ) + if pms_avail_plan_rule_info.closed: + vals.update({"closed": pms_avail_plan_rule_info.closed}) + if pms_avail_plan_rule_info.closedDeparture: + vals.update( + {"closed_departure": pms_avail_plan_rule_info.closedDeparture} + ) + if pms_avail_plan_rule_info.closedArrival: + vals.update({"closed_arrival": pms_avail_plan_rule_info.closedArrival}) + if pms_avail_plan_rule_info.quota: + vals.update({"quota": pms_avail_plan_rule_info.quota}) + if pms_avail_plan_rule_info.maxAvailability: + vals.update({"max_avail": pms_avail_plan_rule_info.maxAvailability}) + avail_rule.write(vals) diff --git a/pms_api_rest/views/res_users_views.xml b/pms_api_rest/views/res_users_views.xml index 9f77bf081b..0049c49065 100644 --- a/pms_api_rest/views/res_users_views.xml +++ b/pms_api_rest/views/res_users_views.xml @@ -15,7 +15,7 @@ name="availability_rule_field_ids" widget="many2many_tags" options="{'no_create': True}" - domain="['&',('model_id', '=', 'pms.availability.plan.rule'), ('name', 'in', ('min_stay', 'max_stay', 'quota', 'max_stay_arrival', 'closed_arrival', 'closed', 'closed_departure'))]" + domain="['&',('model_id', '=', 'pms.availability.plan.rule'), ('name', 'in', ('min_stay', 'max_stay', 'quota', 'max_stay_arrival', 'closed_arrival', 'closed', 'closed_departure', 'min_stay_arrival', 'max_avail'))]" /> From 6fb6e72b4287b6707680f10b40d78ce719533199 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 12 Aug 2022 10:51:25 +0200 Subject: [PATCH 151/547] [FIX]: added default avail rule fields in res users --- pms_api_rest/models/res_users.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index f5a02a57c5..cf821d5448 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -8,7 +8,20 @@ class ResUsers(models.Model): 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", ) + + 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 [] From a9667c789855f910a888479ba7e8717ac68f8c3a Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 10 Aug 2022 19:52:56 +0200 Subject: [PATCH 152/547] [IMP]: changes in avail rules and pricelist items services and datamodels for create/write --- .../datamodels/pms_availability_plan_rule.py | 10 +- pms_api_rest/datamodels/pms_pricelist_item.py | 6 + .../services/pms_availability_plan_service.py | 117 +++++++----------- .../services/pms_pricelist_service.py | 85 ++++++------- 4 files changed, 92 insertions(+), 126 deletions(-) diff --git a/pms_api_rest/datamodels/pms_availability_plan_rule.py b/pms_api_rest/datamodels/pms_availability_plan_rule.py index be534ecd42..183e213a5d 100644 --- a/pms_api_rest/datamodels/pms_availability_plan_rule.py +++ b/pms_api_rest/datamodels/pms_availability_plan_rule.py @@ -1,12 +1,13 @@ 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=True, allow_none=False) - dateTo = fields.String(required=True, allow_none=False) + dateFrom = fields.String(required=False, allow_none=False) + dateTo = fields.String(required=False, allow_none=False) pmsPropertyId = fields.Integer(required=True, allow_none=False) @@ -25,3 +26,8 @@ class PmsAvailabilityPlanRuleInfo(Datamodel): quota = fields.Integer(required=False, allow_none=True) maxAvailability = fields.Integer(required=False, allow_none=True) pmsPropertyId = 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_pricelist_item.py b/pms_api_rest/datamodels/pms_pricelist_item.py index 59df5b8867..3f4944e869 100644 --- a/pms_api_rest/datamodels/pms_pricelist_item.py +++ b/pms_api_rest/datamodels/pms_pricelist_item.py @@ -1,6 +1,7 @@ from marshmallow import fields from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel class PmsPricelistItemSearchParam(Datamodel): @@ -16,4 +17,9 @@ class PmsPricelistItemInfo(Datamodel): 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/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 59d0f83a12..be4f506b13 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -150,89 +150,56 @@ def get_availability_plan_rules( "POST", ) ], - input_param=Datamodel("pms.availability.plan.rule.info", is_list=False), + 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_rule_info + self, availability_plan_id, pms_avail_plan_rules_info ): - day = datetime.strptime( - pms_avail_plan_rule_info.date[:10], "%Y-%m-%d" - ) + timedelta(days=1) - vals = { - "room_type_id": pms_avail_plan_rule_info.roomTypeId, - "date": day, - "pms_property_id": pms_avail_plan_rule_info.pmsPropertyId, - "availability_plan_id": availability_plan_id, - } - - if pms_avail_plan_rule_info.minStay: - vals.update({"min_stay": pms_avail_plan_rule_info.minStay}) - if pms_avail_plan_rule_info.minStayArrival: - vals.update({"min_stay_arrival": pms_avail_plan_rule_info.minStayArrival}) - if pms_avail_plan_rule_info.maxStay: - vals.update({"max_stay": pms_avail_plan_rule_info.maxStay}) - if pms_avail_plan_rule_info.maxStayArrival: - vals.update({"max_stay_arrival": pms_avail_plan_rule_info.maxStayArrival}) - if pms_avail_plan_rule_info.closed: - vals.update({"closed": pms_avail_plan_rule_info.closed}) - if pms_avail_plan_rule_info.closedDeparture: - vals.update({"closed_departure": pms_avail_plan_rule_info.closedDeparture}) - if pms_avail_plan_rule_info.closedArrival: - vals.update({"closed_arrival": pms_avail_plan_rule_info.closedArrival}) - if pms_avail_plan_rule_info.quota: - vals.update({"quota": pms_avail_plan_rule_info.quota}) - if pms_avail_plan_rule_info.maxAvailability: - vals.update({"max_avail": pms_avail_plan_rule_info.maxAvailability}) - avail_plan_rule = self.env["pms.availability.plan.rule"].create(vals) - return avail_plan_rule.id - - @restapi.method( - [ - ( - [ - "//" - "availability-plan-rules/", - ], - "PATCH", - ) - ], - input_param=Datamodel("pms.availability.plan.rule.info", is_list=False), - auth="jwt_api_pms", - ) - def write_availability_plan_rule( - self, availability_plan_id, availability_plan_rule_id, pms_avail_plan_rule_info - ): - avail_rule = self.env["pms.availability.plan.rule"].search( - [ - ("availability_plan_id", "=", availability_plan_id), - ("id", "=", availability_plan_rule_id), - ] - ) - if avail_rule: + for avail_plan_rule in pms_avail_plan_rules_info.availabilityPlanRules: vals = dict() - if pms_avail_plan_rule_info.minStay: - vals.update({"min_stay": pms_avail_plan_rule_info.minStay}) - if pms_avail_plan_rule_info.minStayArrival: + date = datetime.strptime( + avail_plan_rule.date[:10], "%Y-%m-%d" + ) + timedelta(days=1) + if avail_plan_rule.minStay: + vals.update({"min_stay": avail_plan_rule.minStay}) + if avail_plan_rule.minStayArrival: vals.update( - {"min_stay_arrival": pms_avail_plan_rule_info.minStayArrival} + {"min_stay_arrival": avail_plan_rule.minStayArrival} ) - if pms_avail_plan_rule_info.maxStay: - vals.update({"max_stay": pms_avail_plan_rule_info.maxStay}) - if pms_avail_plan_rule_info.maxStayArrival: + if avail_plan_rule.maxStay: + vals.update({"max_stay": avail_plan_rule.maxStay}) + if avail_plan_rule.maxStayArrival: vals.update( - {"max_stay_arrival": pms_avail_plan_rule_info.maxStayArrival} + {"max_stay_arrival": avail_plan_rule.maxStayArrival} ) - if pms_avail_plan_rule_info.closed: - vals.update({"closed": pms_avail_plan_rule_info.closed}) - if pms_avail_plan_rule_info.closedDeparture: + if avail_plan_rule.closed: + vals.update({"closed": avail_plan_rule.closed}) + if avail_plan_rule.closedDeparture: + vals.update( + {"closed_departure": avail_plan_rule.closedDeparture} + ) + if avail_plan_rule.closedArrival: + vals.update({"closed_arrival": avail_plan_rule.closedArrival}) + if avail_plan_rule.quota: + vals.update({"quota": avail_plan_rule.quota}) + avail_rule = self.env["pms.availability.plan.rule"].search( + [ + ("availability_plan_id", "=", availability_plan_id), + ("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( - {"closed_departure": pms_avail_plan_rule_info.closedDeparture} + { + "room_type_id": avail_plan_rule.roomTypeId, + "date": date, + "pms_property_id": avail_plan_rule.pmsPropertyId, + "availability_plan_id": availability_plan_id, + } ) - if pms_avail_plan_rule_info.closedArrival: - vals.update({"closed_arrival": pms_avail_plan_rule_info.closedArrival}) - if pms_avail_plan_rule_info.quota: - vals.update({"quota": pms_avail_plan_rule_info.quota}) - if pms_avail_plan_rule_info.maxAvailability: - vals.update({"max_avail": pms_avail_plan_rule_info.maxAvailability}) - avail_rule.write(vals) + self.env["pms.availability.plan.rule"].create(vals) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 4a5efa9fc7..efbfcd9935 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -151,57 +151,44 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): "POST", ) ], - input_param=Datamodel("pms.pricelist.item.info", is_list=False), + input_param=Datamodel("pms.pricelist.items.info", is_list=False), auth="jwt_api_pms", ) def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): - day = datetime.strptime( - pms_pricelist_item_info.date[:10], "%Y-%m-%d" - ) + timedelta(days=1) - product_id = ( - self.env["pms.room.type"] - .browse(pms_pricelist_item_info.roomTypeId) - .product_id - ) - pricelist_item = self.env["product.pricelist.item"].create( - { - "applied_on": "0_product_variant", - "product_id": product_id.id, - "pms_property_ids": [pms_pricelist_item_info.pmsPropertyId], - "date_start_consumption": day, - "date_end_consumption": day, - "compute_price": "fixed", - "fixed_price": pms_pricelist_item_info.price, - "pricelist_id": pricelist_id, - } - ) - return pricelist_item.id - - @restapi.method( - [ - ( - [ - "//pricelist-items/", - ], - "PATCH", + for pms_pricelist_item in pms_pricelist_item_info.pricelistItems: + date = datetime.strptime( + pms_pricelist_item.date[:10], "%Y-%m-%d" + ) + timedelta(days=1) + product_id = ( + self.env["pms.room.type"] + .browse(pms_pricelist_item.roomTypeId) + .product_id ) - ], - input_param=Datamodel("pms.pricelist.item.info", is_list=False), - auth="jwt_api_pms", - ) - def write_pricelist_item( - self, pricelist_id, pricelist_item_id, pms_pricelist_item_info - ): - - product_pricelist_item = self.env["product.pricelist.item"].search( - [ - ("pricelist_id", "=", pricelist_id), - ("id", "=", pricelist_item_id), - ] - ) - if product_pricelist_item and pms_pricelist_item_info.price: - product_pricelist_item.write( - { - "fixed_price": pms_pricelist_item_info.price, - } + product_pricelist_item = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", pricelist_id), + ("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": pricelist_id, + } + ) From d6f8bdc48a6482da9d0232190442ca342fd9fb2a Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 11 Aug 2022 20:38:57 +0200 Subject: [PATCH 153/547] [IMP]pms_api_rest: added avail plan rule id to avail plan rule service and datamodele --- .../datamodels/pms_availability_plan_rule.py | 1 + pms_api_rest/datamodels/pms_pricelist_item.py | 1 + .../services/pms_availability_plan_service.py | 22 +++++++------------ .../services/pms_pricelist_service.py | 6 ++--- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/pms_api_rest/datamodels/pms_availability_plan_rule.py b/pms_api_rest/datamodels/pms_availability_plan_rule.py index 183e213a5d..632ba300d7 100644 --- a/pms_api_rest/datamodels/pms_availability_plan_rule.py +++ b/pms_api_rest/datamodels/pms_availability_plan_rule.py @@ -26,6 +26,7 @@ class PmsAvailabilityPlanRuleInfo(Datamodel): 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): diff --git a/pms_api_rest/datamodels/pms_pricelist_item.py b/pms_api_rest/datamodels/pms_pricelist_item.py index 3f4944e869..b729dadedc 100644 --- a/pms_api_rest/datamodels/pms_pricelist_item.py +++ b/pms_api_rest/datamodels/pms_pricelist_item.py @@ -20,6 +20,7 @@ class PmsPricelistItemInfo(Datamodel): 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/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index be4f506b13..9e53f9b139 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -158,34 +158,28 @@ def create_availability_plan_rule( ): for avail_plan_rule in pms_avail_plan_rules_info.availabilityPlanRules: vals = dict() - date = datetime.strptime( - avail_plan_rule.date[:10], "%Y-%m-%d" - ) + timedelta(days=1) + date = datetime.strptime(avail_plan_rule.date[:10], "%Y-%m-%d") + timedelta( + days=1 + ) if avail_plan_rule.minStay: vals.update({"min_stay": avail_plan_rule.minStay}) if avail_plan_rule.minStayArrival: - vals.update( - {"min_stay_arrival": avail_plan_rule.minStayArrival} - ) + vals.update({"min_stay_arrival": avail_plan_rule.minStayArrival}) if avail_plan_rule.maxStay: vals.update({"max_stay": avail_plan_rule.maxStay}) if avail_plan_rule.maxStayArrival: - vals.update( - {"max_stay_arrival": avail_plan_rule.maxStayArrival} - ) + vals.update({"max_stay_arrival": avail_plan_rule.maxStayArrival}) if avail_plan_rule.closed: vals.update({"closed": avail_plan_rule.closed}) if avail_plan_rule.closedDeparture: - vals.update( - {"closed_departure": avail_plan_rule.closedDeparture} - ) + vals.update({"closed_departure": avail_plan_rule.closedDeparture}) if avail_plan_rule.closedArrival: vals.update({"closed_arrival": avail_plan_rule.closedArrival}) if avail_plan_rule.quota: vals.update({"quota": avail_plan_rule.quota}) avail_rule = self.env["pms.availability.plan.rule"].search( [ - ("availability_plan_id", "=", availability_plan_id), + ("availability_plan_id", "=", avail_plan_rule.availabilityPlanId), ("pms_property_id", "=", avail_plan_rule.pmsPropertyId), ("room_type_id", "=", avail_plan_rule.roomTypeId), ("date", "=", date), @@ -199,7 +193,7 @@ def create_availability_plan_rule( "room_type_id": avail_plan_rule.roomTypeId, "date": date, "pms_property_id": avail_plan_rule.pmsPropertyId, - "availability_plan_id": availability_plan_id, + "availability_plan_id": avail_plan_rule.availabilityPlanId, } ) self.env["pms.availability.plan.rule"].create(vals) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index efbfcd9935..5a7be8ca1f 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -166,11 +166,11 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): ) product_pricelist_item = self.env["product.pricelist.item"].search( [ - ("pricelist_id", "=", pricelist_id), + ("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), + ("date_end_consumption", "=", date), ] ) if product_pricelist_item: @@ -189,6 +189,6 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): "date_end_consumption": date, "compute_price": "fixed", "fixed_price": pms_pricelist_item.price, - "pricelist_id": pricelist_id, + "pricelist_id": pms_pricelist_item.pricelistId, } ) From 96c7949adc7dc797f50e8161466af76f4542b0ee Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 24 Aug 2022 17:58:51 +0200 Subject: [PATCH 154/547] [FIX]pms_api_rest: fixed avail rules and pricelist items post --- .../services/pms_availability_plan_service.py | 20 +++++++++---------- .../services/pms_pricelist_service.py | 4 +--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 9e53f9b139..8b0a4e2ce7 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -158,24 +158,22 @@ def create_availability_plan_rule( ): for avail_plan_rule in pms_avail_plan_rules_info.availabilityPlanRules: vals = dict() - date = datetime.strptime(avail_plan_rule.date[:10], "%Y-%m-%d") + timedelta( - days=1 - ) - if avail_plan_rule.minStay: + 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: + if avail_plan_rule.minStayArrival is not None: vals.update({"min_stay_arrival": avail_plan_rule.minStayArrival}) - if avail_plan_rule.maxStay: + if avail_plan_rule.maxStay is not None: vals.update({"max_stay": avail_plan_rule.maxStay}) - if avail_plan_rule.maxStayArrival: + if avail_plan_rule.maxStayArrival is not None: vals.update({"max_stay_arrival": avail_plan_rule.maxStayArrival}) - if avail_plan_rule.closed: + if avail_plan_rule.closed is not None: vals.update({"closed": avail_plan_rule.closed}) - if avail_plan_rule.closedDeparture: + if avail_plan_rule.closedDeparture is not None: vals.update({"closed_departure": avail_plan_rule.closedDeparture}) - if avail_plan_rule.closedArrival: + if avail_plan_rule.closedArrival is not None: vals.update({"closed_arrival": avail_plan_rule.closedArrival}) - if avail_plan_rule.quota: + if avail_plan_rule.quota is not None: vals.update({"quota": avail_plan_rule.quota}) avail_rule = self.env["pms.availability.plan.rule"].search( [ diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 5a7be8ca1f..3b400cc51b 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -156,9 +156,7 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): ) def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): for pms_pricelist_item in pms_pricelist_item_info.pricelistItems: - date = datetime.strptime( - pms_pricelist_item.date[:10], "%Y-%m-%d" - ) + timedelta(days=1) + date = datetime.strptime(pms_pricelist_item.date, "%Y-%m-%d").date() product_id = ( self.env["pms.room.type"] .browse(pms_pricelist_item.roomTypeId) From cbf1b0bb72aa35dfe58a27cf625f9904329b7cc6 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 25 Aug 2022 22:20:15 +0200 Subject: [PATCH 155/547] [REF]pms_api_rest: added max availability in avail plan rule post service --- pms_api_rest/services/pms_availability_plan_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 8b0a4e2ce7..425363f265 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -175,6 +175,8 @@ def create_availability_plan_rule( 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), From c34ffac836cf6aeea760dc3e57c8f4b3ea7b8939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 1 Sep 2022 17:12:36 +0200 Subject: [PATCH 156/547] [IMP]pms_api_rest: get pricelists improvements (refact + saleChannelId param) --- pms_api_rest/datamodels/pms_pricelist.py | 9 +++ .../services/pms_pricelist_service.py | 61 ++++++++++--------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/pms_api_rest/datamodels/pms_pricelist.py b/pms_api_rest/datamodels/pms_pricelist.py index 0fd0554daf..cd023db489 100644 --- a/pms_api_rest/datamodels/pms_pricelist.py +++ b/pms_api_rest/datamodels/pms_pricelist.py @@ -3,6 +3,14 @@ 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) + + class PmsPricelistInfo(Datamodel): _name = "pms.pricelist.info" id = fields.Integer(required=False, allow_none=True) @@ -10,3 +18,4 @@ class PmsPricelistInfo(Datamodel): 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/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 3b400cc51b..3a9a3c01db 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -1,7 +1,7 @@ -import re from datetime import datetime, timedelta -from odoo.exceptions import MissingError +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 @@ -23,39 +23,42 @@ class PmsPricelistService(Component): "GET", ) ], - input_param=Datamodel("pms.search.param", is_list=False), + 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): - - pricelists_all_properties = self.env["product.pricelist"].search( - [("pms_property_ids", "=", False)] - ) + pricelists = self.env["product.pricelist"].search([]) + 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 = set() - for index, prop in enumerate(pms_search_param.pmsPropertyIds): - pricelists_with_query_property = self.env["product.pricelist"].search( - [("pms_property_ids", "=", prop)] + 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 index == 0: - pricelists = set(pricelists_with_query_property.ids) - else: - pricelists = pricelists.intersection( - set(pricelists_with_query_property.ids) - ) - pricelists_total = list( - set(list(pricelists) + pricelists_all_properties.ids) ) - else: - pricelists_total = list(pricelists_all_properties.ids) - domain = [ - ("id", "in", pricelists_total), - ] - + 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 self.env["product.pricelist"].search(domain): + for pricelist in pricelists: result_pricelists.append( PmsPricelistInfo( id=pricelist.id, @@ -66,7 +69,8 @@ def get_pricelists(self, pms_search_param, **args): defaultAvailabilityPlanId=pricelist.availability_plan_id.id if pricelist.availability_plan_id else None, - pmsPropertyIds=pricelist.pms_property_ids.mapped("id"), + pmsPropertyIds=pricelist.pms_property_ids.ids, + saleChannelIds=pricelist.pms_sale_channel_ids.ids, ) ) return result_pricelists @@ -135,8 +139,7 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): ) pricelist_info.pricelistItemId = item.id - price = re.findall(r"[+-]?\d+\.\d+", item.price) - pricelist_info.price = float(price[0]) + pricelist_info.price = item.fixed_price result.append(pricelist_info) From abc7ea5f2d40551cbea821d6ca88059de03b7e3c Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 13 Sep 2022 10:31:23 +0200 Subject: [PATCH 157/547] [FIX] pms-api-rest: room type class & adults if none @ services --- pms_api_rest/datamodels/pms_room_type.py | 1 + pms_api_rest/services/pms_reservation_service.py | 2 +- pms_api_rest/services/pms_room_type_service.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_room_type.py b/pms_api_rest/datamodels/pms_room_type.py index 314a143c5b..48c4370baf 100644 --- a/pms_api_rest/datamodels/pms_room_type.py +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -15,4 +15,5 @@ class PmsRoomTypeInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 0dea752b16..7671bccb2b 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -68,7 +68,7 @@ def get_reservation(self, reservation_id, pms_search_param): if reservation.channel_type_id else None, agencyId=reservation.agency_id.id if reservation.agency_id else None, - userId=reservation.user_id.id, + userId=reservation.user_id.id if reservation.user_id else None, checkin=datetime.combine( reservation.checkin, datetime.min.time() ).isoformat(), diff --git a/pms_api_rest/services/pms_room_type_service.py b/pms_api_rest/services/pms_room_type_service.py index 4615fa1a79..6f5dc5c25c 100644 --- a/pms_api_rest/services/pms_room_type_service.py +++ b/pms_api_rest/services/pms_room_type_service.py @@ -60,6 +60,7 @@ def get_room_types(self, room_type_search_param): pmsPropertyIds=room.pms_property_ids.mapped("id"), defaultCode=room.default_code, price=round(room.list_price, 2), + classId=room.class_id, ) ) return result_rooms From 039b3f215a6a6668cef88b1f8afce4d10360ebb8 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 31 Aug 2022 19:12:12 +0200 Subject: [PATCH 158/547] [IMP]pms_api_rest: added post and patch in partner service --- pms_api_rest/datamodels/pms_partner.py | 32 ++++ pms_api_rest/services/pms_partner_service.py | 174 +++++++++++++++++-- 2 files changed, 195 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index 03f4cb3a5b..9e1fa30d6a 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -3,7 +3,39 @@ from odoo.addons.datamodel.core import Datamodel +class PmsPartnerSearchParam(Datamodel): + _name = "pms.partner.search.param" + id = fields.Integer(required=False, allow_none=True) + vat = 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) + vat = 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) + 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) + isAgency = fields.Boolean(required=False, allow_none=True) + countryChar = fields.String(required=False, allow_none=True) + countryId = fields.Integer(required=False, allow_none=True) + countryName = fields.String(required=False, allow_none=True) + tagIds = fields.List(fields.Integer(required=False, allow_none=True)) + documentNumbers = fields.List(fields.Integer(required=False, allow_none=True)) + lastStay = fields.String(required=False, allow_none=True) + website = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 8153507364..0de2934870 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -1,3 +1,5 @@ +from datetime import datetime + from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component @@ -18,25 +20,150 @@ class PmsPartnerService(Component): "GET", ) ], + input_param=Datamodel("pms.partner.search.param", is_list=False), output_param=Datamodel("pms.partner.info", is_list=True), auth="jwt_api_pms", ) - def get_partners(self): - domain = [] + def get_partners(self, pms_partner_search_params): result_partners = [] + domain = [] + dni = "" + if pms_partner_search_params.vat: + domain.append(("vat", "=", pms_partner_search_params.vat)) PmsPartnerInfo = self.env.datamodels["pms.partner.info"] - for partner in self.env["res.partner"].search( - domain, - ): - + for partner in self.env["res.partner"].search(domain): + if partner.id_numbers: + doc_type_id = ( + self.env["res.partner.id_category"] + .search([("name", "=", "DNI")]) + .id + ) + dni = ( + self.env["res.partner.id_number"] + .search( + [ + ("partner_id", "=", partner.id), + ("category_id", "=", doc_type_id), + ] + ) + .name + ) 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, + mobile=partner.mobile if partner.mobile else None, + phone=partner.phone if partner.phone else None, + birthdate=datetime.combine( + partner.birthdate_date, datetime.min.time() + ).isoformat() + if partner.birthdate_date + else None, + residenceStreet=partner.residence_street + if partner.residence_street + else None, + zip=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, + countryState=partner.residence_state_id.id + if partner.residence_state_id + else None, + isAgency=partner.is_agency, + countryId=partner.residence_country_id.id + if partner.residence_country_id + else None, + countryChar=partner.residence_country_id.code_alpha3 + if partner.residence_country_id + else None, + countryName=partner.residence_country_id.name + if partner.residence_country_id + else None, + tagIds=partner.category_id.ids if partner.category_id else [], + documentNumber=dni if dni else None, + documentNumbers=partner.id_numbers if partner.id_numbers else [], + vat=partner.vat if partner.vat else None, + website=partner.website if partner.website else None, ) ) return result_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) + if partner_info.documentNumber: + doc_type_id = ( + self.env["res.partner.id_category"].search([("name", "=", "DNI")]).id + ) + self.env["res.partner.id_number"].create( + { + "partner_id": partner.id, + "category_id": doc_type_id, + "name": partner_info.documentNumber, + } + ) + + @restapi.method( + [ + ( + [ + "/", + ], + "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)) + if partner_info.documentNumber: + doc_type_id = ( + self.env["res.partner.id_category"].search([("name", "=", "DNI")]).id + ) + doc_number = self.env["res.partner.id_number"].search( + [ + ("partner_id", "=", partner_id), + ("name", "=", partner_info.documentNumber), + ("category_id", "=", doc_type_id), + ] + ) + if not doc_number: + self.env["res.partner.id_number"].create( + { + "category_id": doc_type_id, + "name": partner_info.documentNumber, + } + ) + else: + doc_number.write( + { + "name": partner_info.documentNumber, + } + ) + + # REVIEW: analyze in which service file this method should be @restapi.method( [ ( @@ -50,11 +177,8 @@ def get_partners(self): auth="jwt_api_pms", ) def get_partner_by_doc_number(self, document_type, document_number): - doc_type = self.env["res.partner.id_category"].search( - [("id", "=", document_type)] - ) doc_number = self.env["res.partner.id_number"].search( - [("name", "=", document_number), ("category_id", "=", doc_type.id)] + [("name", "=", document_number), ("category_id", "=", int(document_type))] ) partners = [] PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] @@ -88,7 +212,7 @@ def get_partner_by_doc_number(self, document_type, document_number): mobile=doc_number.partner_id.mobile if doc_number.partner_id.mobile else None, - documentType=doc_type.id, + documentType=int(document_type), documentNumber=doc_number.name, documentExpeditionDate=document_expedition_date if doc_number.valid_from @@ -120,3 +244,31 @@ def get_partner_by_doc_number(self, document_type, document_number): ) ) return partners + + def mapping_partner_values(self, pms_partner_info): + vals = dict() + partner_fields = { + "firstname": pms_partner_info.firstname, + "lastname": pms_partner_info.lastname, + "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, + "nationality_id": pms_partner_info.nationality, + "residence_zip": pms_partner_info.zip, + "residence_city": pms_partner_info.residenceCity, + "residence_state_id": pms_partner_info.countryState, + "residence_country_id": pms_partner_info.nationality, + "is_agency": pms_partner_info.isAgency, + "vat": pms_partner_info.vat, + "website": pms_partner_info.website, + } + 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: + vals.update({k: v}) + return vals From 17a204281d5286a6380ceda6d6811848da3f3f5a Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 16 Sep 2022 10:55:42 +0200 Subject: [PATCH 159/547] [IMP]: pms-api-rest: add channel type @ sale channels --- pms_api_rest/datamodels/pms_sale_channel.py | 1 + pms_api_rest/services/pms_sale_channel_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_sale_channel.py b/pms_api_rest/datamodels/pms_sale_channel.py index 77a82e5d88..8a734b422a 100644 --- a/pms_api_rest/datamodels/pms_sale_channel.py +++ b/pms_api_rest/datamodels/pms_sale_channel.py @@ -12,3 +12,4 @@ 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) diff --git a/pms_api_rest/services/pms_sale_channel_service.py b/pms_api_rest/services/pms_sale_channel_service.py index 16f774e93d..a47d2eee95 100644 --- a/pms_api_rest/services/pms_sale_channel_service.py +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -59,6 +59,7 @@ def get_sale_channels(self, sale_channel_search_param): 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, ) ) return result_sale_channels From 19b3a5a23d3bc19009fd110473da56b22e2035bd Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 16 Sep 2022 11:02:05 +0200 Subject: [PATCH 160/547] [FIX] pms-api-rest: fix precommit --- pms_api_rest/services/pms_price_service.py | 36 ++++++++++++------- .../services/pms_sale_channel_service.py | 4 ++- .../services/pms_service_line_service.py | 17 +++++---- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/pms_api_rest/services/pms_price_service.py b/pms_api_rest/services/pms_price_service.py index 0f829aa0e4..a7a121896e 100644 --- a/pms_api_rest/services/pms_price_service.py +++ b/pms_api_rest/services/pms_price_service.py @@ -1,6 +1,7 @@ +from datetime import datetime, timedelta + from odoo import _, fields from odoo.exceptions import MissingError -from datetime import datetime, timedelta from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -29,15 +30,24 @@ class PmsAgencyService(Component): def get_prices(self, prices_search_param): product = room_type = board_service = False if prices_search_param.roomTypeId: - room_type = self.env["pms.room.type"].search([("id", "=", prices_search_param.roomTypeId)]) + room_type = self.env["pms.room.type"].search( + [("id", "=", prices_search_param.roomTypeId)] + ) if prices_search_param.productId: - product = self.env["product.product"].search([("id", "=", prices_search_param.productId)]) + product = self.env["product.product"].search( + [("id", "=", prices_search_param.productId)] + ) if prices_search_param.boardServiceId: - board_service = self.env["pms.board.service.room.type"].search([ - ("id", "=", prices_search_param.boardServiceId)] + board_service = self.env["pms.board.service.room.type"].search( + [("id", "=", prices_search_param.boardServiceId)] ) if sum([var is not False for var in [product, room_type, board_service]]) != 1: - raise MissingError(_("It is necessary to indicate one and only one product, board service or room type")) + raise MissingError( + _( + "It is necessary to indicate one and only one product," + " board service or room type" + ) + ) PmsPriceInfo = self.env.datamodels["pms.price.info"] result_prices = [] @@ -61,8 +71,9 @@ def get_prices(self, prices_search_param): pricelist_id=prices_search_param.pricelistId, partner_id=prices_search_param.partnerId, product_qty=prices_search_param.productQty, - date_consumption=price_date - ), 2 + date_consumption=price_date, + ), + 2, ), ) ) @@ -79,8 +90,9 @@ def get_prices(self, prices_search_param): pricelist_id=prices_search_param.pricelistId, partner_id=prices_search_param.partnerId, product_qty=prices_search_param.productQty, - date_consumption=price_date - ), 2 + date_consumption=price_date, + ), + 2, ), ) ) @@ -96,9 +108,7 @@ def _get_product_price( date_consumption=False, board_service_id=False, ): - pms_property = self.env["pms.property"].browse( - pms_property_id - ) + pms_property = self.env["pms.property"].browse(pms_property_id) product_context = dict( self.env.context, date=datetime.today().date(), diff --git a/pms_api_rest/services/pms_sale_channel_service.py b/pms_api_rest/services/pms_sale_channel_service.py index a47d2eee95..5d6dd7a887 100644 --- a/pms_api_rest/services/pms_sale_channel_service.py +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -59,7 +59,9 @@ def get_sale_channels(self, sale_channel_search_param): 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, + channelType=sale_channel.channel_type + if sale_channel.channel_type + else None, ) ) return result_sale_channels diff --git a/pms_api_rest/services/pms_service_line_service.py b/pms_api_rest/services/pms_service_line_service.py index 6d5f4ec968..a24f225337 100644 --- a/pms_api_rest/services/pms_service_line_service.py +++ b/pms_api_rest/services/pms_service_line_service.py @@ -27,16 +27,16 @@ class PmsServiceLineService(Component): auth="jwt_api_pms", ) def get_service_line(self, service_line_id): - service_line = self.env["pms.service.line"].search([("id", "=", 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(), + 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, @@ -55,7 +55,9 @@ def get_service_line(self, service_line_id): 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)]) + service_line = self.env["pms.service.line"].search( + [("id", "=", service_line_id)] + ) vals = {} if service_line: if pms_service_line_info_data.date: @@ -85,9 +87,10 @@ def update_service_line(self, service_line_id, pms_service_line_info_data): ) 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)]) + 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")) - From d4f200cf4199cb37caacde769aae1b35cc4ad37c Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 26 Sep 2022 10:55:57 +0200 Subject: [PATCH 161/547] [IMP] pms-api-rest: partner search param --- pms_api_rest/datamodels/pms_partner.py | 1 + pms_api_rest/services/pms_partner_service.py | 20 ++------------------ 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index 9e1fa30d6a..45bddb0475 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -7,6 +7,7 @@ class PmsPartnerSearchParam(Datamodel): _name = "pms.partner.search.param" id = fields.Integer(required=False, allow_none=True) vat = fields.String(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) class PmsPartnerInfo(Datamodel): diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 0de2934870..f2ed9a7ade 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -27,27 +27,12 @@ class PmsPartnerService(Component): def get_partners(self, pms_partner_search_params): result_partners = [] domain = [] - dni = "" if pms_partner_search_params.vat: domain.append(("vat", "=", pms_partner_search_params.vat)) + if pms_partner_search_params.name: + domain.append(("name", "ilike", pms_partner_search_params.name)) PmsPartnerInfo = self.env.datamodels["pms.partner.info"] for partner in self.env["res.partner"].search(domain): - if partner.id_numbers: - doc_type_id = ( - self.env["res.partner.id_category"] - .search([("name", "=", "DNI")]) - .id - ) - dni = ( - self.env["res.partner.id_number"] - .search( - [ - ("partner_id", "=", partner.id), - ("category_id", "=", doc_type_id), - ] - ) - .name - ) result_partners.append( PmsPartnerInfo( id=partner.id, @@ -87,7 +72,6 @@ def get_partners(self, pms_partner_search_params): if partner.residence_country_id else None, tagIds=partner.category_id.ids if partner.category_id else [], - documentNumber=dni if dni else None, documentNumbers=partner.id_numbers if partner.id_numbers else [], vat=partner.vat if partner.vat else None, website=partner.website if partner.website else None, From 5d40918aaaf0eb097b53140327eb3b0afdb75a2a Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 21 Apr 2022 18:40:54 +0200 Subject: [PATCH 162/547] [IMP]pms: added configuration and color code for reservation segments in the property --- pms/models/pms_property.py | 79 ++++++++++++++++++++++++++++++++ pms/views/pms_property_views.xml | 67 +++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index 63ff0ee50c..cb699f7119 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -243,6 +243,85 @@ class PmsProperty(models.Model): default=False, ) + 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)", + ) + @api.depends_context( "checkin", "checkout", diff --git a/pms/views/pms_property_views.xml b/pms/views/pms_property_views.xml index 415f260326..8da488f716 100644 --- a/pms/views/pms_property_views.xml +++ b/pms/views/pms_property_views.xml @@ -88,6 +88,73 @@ + + + + + + + + + + + + + + + + + + + From 8c9918a5574676fabb67e61a1b3bba1d5dadb1ca Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 27 Apr 2022 11:38:36 +0200 Subject: [PATCH 163/547] [REF]pms_api_rest: changed property colors from module pms to module pms_api_rest --- pms/models/pms_property.py | 79 -------------------------------- pms/views/pms_property_views.xml | 67 --------------------------- 2 files changed, 146 deletions(-) diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index cb699f7119..63ff0ee50c 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -243,85 +243,6 @@ class PmsProperty(models.Model): default=False, ) - 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)", - ) - @api.depends_context( "checkin", "checkout", diff --git a/pms/views/pms_property_views.xml b/pms/views/pms_property_views.xml index 8da488f716..415f260326 100644 --- a/pms/views/pms_property_views.xml +++ b/pms/views/pms_property_views.xml @@ -88,73 +88,6 @@ - - - - - - - - - - - - - - - - - - - From 0f8e22a890a2978db3f964d920318c175f8046eb Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 19 Aug 2022 19:59:16 +0200 Subject: [PATCH 164/547] [IMP]pms-api-rest: added post for folio charges --- pms_api_rest/datamodels/__init__.py | 2 +- .../datamodels/pms_account_payment.py | 20 +++++ pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/datamodels/pms_payment.py | 11 --- pms_api_rest/services/pms_folio_service.py | 79 +++++++++++++++---- 5 files changed, 86 insertions(+), 27 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_account_payment.py delete mode 100644 pms_api_rest/datamodels/pms_payment.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index bf04d41602..981d688d63 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -14,7 +14,7 @@ from . import pms_property from . import pms_account_journal -from . import pms_payment +from . import pms_account_payment from . import pms_user diff --git a/pms_api_rest/datamodels/pms_account_payment.py b/pms_api_rest/datamodels/pms_account_payment.py new file mode 100644 index 0000000000..4f0c7b8dba --- /dev/null +++ b/pms_api_rest/datamodels/pms_account_payment.py @@ -0,0 +1,20 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPaymentInfo(Datamodel): + _name = "pms.payment.info" + id = fields.Integer(required=False, allow_none=True) + amount = fields.Float(required=False, allow_none=True) + journalId = fields.Integer(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + + +class PmsAccountPaymentInfo(Datamodel): + _name = "pms.account.payment.short.info" + id = fields.Integer(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) + partnerName = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 53bba30a87..6536cdd054 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -22,6 +22,7 @@ class PmsFolioInfo(Datamodel): 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) class PmsFolioShortInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_payment.py b/pms_api_rest/datamodels/pms_payment.py deleted file mode 100644 index 728870847d..0000000000 --- a/pms_api_rest/datamodels/pms_payment.py +++ /dev/null @@ -1,11 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsPaymentInfo(Datamodel): - _name = "pms.payment.info" - id = fields.Integer(required=False, allow_none=True) - amount = fields.Float(required=False, allow_none=True) - journalId = fields.Integer(required=False, allow_none=True) - date = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index df5d246595..7ebc7ce08b 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -46,6 +46,7 @@ def get_folio(self, folio_id): ], amountTotal=round(folio.amount_total, 2), reservationType=folio.reservation_type, + pendingAmount=folio.pending_amount, ) else: raise MissingError(_("Folio not found")) @@ -189,8 +190,20 @@ def get_folio_payments(self, folio_id, pms_search_param): pass # si el folio está sin pagar no tendrá ningún pago o envíar []? else: - if folio.statement_line_ids: - for payment in folio.statement_line_ids: + # if folio.statement_line_ids: + # for payment in folio.statement_line_ids: + # payments.append( + # PmsPaymentInfo( + # id=payment.id, + # amount=round(payment.amount, 2), + # journalId=payment.journal_id.id, + # date=datetime.combine( + # payment.date, datetime.min.time() + # ).isoformat(), + # ) + # ) + if folio.payment_ids: + for payment in folio.payment_ids: payments.append( PmsPaymentInfo( id=payment.id, @@ -201,21 +214,57 @@ def get_folio_payments(self, folio_id, pms_search_param): ).isoformat(), ) ) - if folio.payment_ids: - if folio.payment_ids: - for payment in folio.payment_ids: - payments.append( - PmsPaymentInfo( - id=payment.id, - amount=round(payment.amount, 2), - journalId=payment.journal_id.id, - date=datetime.combine( - payment.date, datetime.min.time() - ).isoformat(), - ) - ) return payments + @restapi.method( + [ + ( + [ + "//payments", + ], + "POST", + ) + ], + input_param=Datamodel("pms.account.payment.short.info", is_list=False), + auth="jwt_api_pms", + ) + def create_folio_charge(self, folio_id, pms_account_payment_info): + partner = self.env["res.partner"].search( + [("name", "=", pms_account_payment_info.partnerName)] + ) + folio = self.env["pms.folio"].browse(folio_id) + + if partner: + partner_id = partner + elif not partner and pms_account_payment_info.partnerName: + partner_id = self.env["res.partner"].create( + {"name": pms_account_payment_info.partnerName} + ) + else: + partner_id = folio.partner_id + charge_date_str = ( + pms_account_payment_info.date[3:5] + + "/" + + pms_account_payment_info.date[:2] + + "/" + + pms_account_payment_info.date[6:] + ) + charge_date = datetime.strptime(charge_date_str, "%m/%d/%Y") + journal_id = self.env["account.journal"].browse( + pms_account_payment_info.journalId + ) + self.env["pms.folio"].do_payment( + journal_id, + journal_id.suspense_account_id, + self.env.user, + pms_account_payment_info.amount, + folio, + reservations=False, + services=False, + partner=partner_id, + date=charge_date, + ) + @restapi.method( [ ( From 9629a426f66d667bfa4ed6d20ef1b28fbb53fd8b Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 25 Aug 2022 23:04:42 +0200 Subject: [PATCH 165/547] [IMP]pms_api_rest: added services and datamodels to account payment, folio and partner --- .../datamodels/pms_account_payment.py | 4 +- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/datamodels/pms_partner.py | 7 +- pms_api_rest/services/pms_folio_service.py | 98 ++++++++++--------- pms_api_rest/services/pms_partner_service.py | 1 + 5 files changed, 63 insertions(+), 48 deletions(-) diff --git a/pms_api_rest/datamodels/pms_account_payment.py b/pms_api_rest/datamodels/pms_account_payment.py index 4f0c7b8dba..a42f79aff7 100644 --- a/pms_api_rest/datamodels/pms_account_payment.py +++ b/pms_api_rest/datamodels/pms_account_payment.py @@ -9,6 +9,7 @@ class PmsPaymentInfo(Datamodel): amount = fields.Float(required=False, allow_none=True) journalId = fields.Integer(required=False, allow_none=True) date = fields.String(required=False, allow_none=True) + paymentType = fields.String(required=False, allow_none=True) class PmsAccountPaymentInfo(Datamodel): @@ -17,4 +18,5 @@ class PmsAccountPaymentInfo(Datamodel): date = fields.String(required=False, allow_none=True) journalId = fields.Integer(required=False, allow_none=True) amount = fields.Float(required=False, allow_none=True) - partnerName = fields.String(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) + reservationIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 6536cdd054..8d20019adb 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -23,6 +23,7 @@ class PmsFolioInfo(Datamodel): amountTotal = fields.Float(required=False, allow_none=True) reservationType = fields.String(required=False, allow_none=True) pendingAmount = fields.Float(required=False, allow_none=True) + lastCheckout = fields.String(required=False, allow_none=True) class PmsFolioShortInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index 45bddb0475..e220d55293 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -2,6 +2,11 @@ from odoo.addons.datamodel.core import Datamodel +class PmsPartnerSearchParam(Datamodel): + _name = "pms.partner.search.param" + id = fields.Integer(required=False, allow_none=True) + vat = fields.String(required=False, allow_none=True) + class PmsPartnerSearchParam(Datamodel): _name = "pms.partner.search.param" @@ -37,6 +42,6 @@ class PmsPartnerInfo(Datamodel): countryId = fields.Integer(required=False, allow_none=True) countryName = fields.String(required=False, allow_none=True) tagIds = fields.List(fields.Integer(required=False, allow_none=True)) - documentNumbers = fields.List(fields.Integer(required=False, allow_none=True)) lastStay = fields.String(required=False, allow_none=True) website = fields.String(required=False, allow_none=True) + diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 7ebc7ce08b..fae0b3fd40 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -47,6 +47,7 @@ def get_folio(self, folio_id): amountTotal=round(folio.amount_total, 2), reservationType=folio.reservation_type, pendingAmount=folio.pending_amount, + lastCheckout=str(folio.last_checkout) ) else: raise MissingError(_("Folio not found")) @@ -186,41 +187,29 @@ def get_folio_payments(self, folio_id, pms_search_param): if not folio: pass else: - if folio.payment_state == "not_paid": - pass - # si el folio está sin pagar no tendrá ningún pago o envíar []? - else: - # if folio.statement_line_ids: - # for payment in folio.statement_line_ids: - # payments.append( - # PmsPaymentInfo( - # id=payment.id, - # amount=round(payment.amount, 2), - # journalId=payment.journal_id.id, - # date=datetime.combine( - # payment.date, datetime.min.time() - # ).isoformat(), - # ) - # ) - if folio.payment_ids: - for payment in folio.payment_ids: - payments.append( - PmsPaymentInfo( - id=payment.id, - amount=round(payment.amount, 2), - journalId=payment.journal_id.id, - date=datetime.combine( - payment.date, datetime.min.time() - ).isoformat(), - ) + # if folio.payment_state == "not_paid": + # pass + # else: + if folio.payment_ids: + for payment in folio.payment_ids: + payments.append( + PmsPaymentInfo( + id=payment.id, + amount=round(payment.amount, 2), + journalId=payment.journal_id.id, + date=datetime.combine( + payment.date, datetime.min.time() + ).isoformat(), + paymentType=payment.payment_type, ) + ) return payments @restapi.method( [ ( [ - "//payments", + "//charge", ], "POST", ) @@ -229,31 +218,48 @@ def get_folio_payments(self, folio_id, pms_search_param): auth="jwt_api_pms", ) def create_folio_charge(self, folio_id, pms_account_payment_info): - partner = self.env["res.partner"].search( - [("name", "=", pms_account_payment_info.partnerName)] - ) folio = self.env["pms.folio"].browse(folio_id) + if pms_account_payment_info.partnerId: + partner_id = self.env["res.partner"].browse(int(pms_account_payment_info.partnerId)) + else: + partner_id = folio.partner_id + journal_id = self.env["account.journal"].browse( + pms_account_payment_info.journalId + ) + self.env["pms.folio"].do_payment( + journal_id, + journal_id.suspense_account_id, + self.env.user, + pms_account_payment_info.amount, + folio, + reservations=pms_account_payment_info.reservationIds, + services=False, + partner=partner_id, + date=datetime.strptime(pms_account_payment_info.date, "%m/%d/%Y"), + ) - if partner: - partner_id = partner - elif not partner and pms_account_payment_info.partnerName: - partner_id = self.env["res.partner"].create( - {"name": pms_account_payment_info.partnerName} + @restapi.method( + [ + ( + [ + "//refund", + ], + "POST", ) + ], + input_param=Datamodel("pms.account.payment.short.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) + if pms_account_payment_info.partnerId: + partner_id = self.env["res.partner"].browse(pms_account_payment_info.partnerId) else: partner_id = folio.partner_id - charge_date_str = ( - pms_account_payment_info.date[3:5] - + "/" - + pms_account_payment_info.date[:2] - + "/" - + pms_account_payment_info.date[6:] - ) - charge_date = datetime.strptime(charge_date_str, "%m/%d/%Y") journal_id = self.env["account.journal"].browse( pms_account_payment_info.journalId ) - self.env["pms.folio"].do_payment( + self.env["pms.folio"].do_refund( journal_id, journal_id.suspense_account_id, self.env.user, @@ -262,7 +268,7 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): reservations=False, services=False, partner=partner_id, - date=charge_date, + date=datetime.strptime(pms_account_payment_info.date, "%m/%d/%Y"), ) @restapi.method( diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index f2ed9a7ade..72a4958fe6 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -79,6 +79,7 @@ def get_partners(self, pms_partner_search_params): ) return result_partners + # REVIEW: analyze in which service file this method should be @restapi.method( [ ( From 6f957f5a0d5a44e613386c5a6df004eafd4ca951 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 31 Aug 2022 18:35:01 +0200 Subject: [PATCH 166/547] [IMP]pms_api_rest: changes in account_payment and folio service --- pms_api_rest/datamodels/pms_account_payment.py | 1 + pms_api_rest/datamodels/pms_partner.py | 1 + pms_api_rest/services/pms_folio_service.py | 13 ++++--------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/pms_api_rest/datamodels/pms_account_payment.py b/pms_api_rest/datamodels/pms_account_payment.py index a42f79aff7..5723e3d8c3 100644 --- a/pms_api_rest/datamodels/pms_account_payment.py +++ b/pms_api_rest/datamodels/pms_account_payment.py @@ -19,4 +19,5 @@ class PmsAccountPaymentInfo(Datamodel): journalId = fields.Integer(required=False, allow_none=True) amount = fields.Float(required=False, allow_none=True) partnerId = fields.Integer(required=False, allow_none=True) + reference = fields.String(required=False, allow_none=True) reservationIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index e220d55293..cad2afabb8 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -2,6 +2,7 @@ from odoo.addons.datamodel.core import Datamodel + class PmsPartnerSearchParam(Datamodel): _name = "pms.partner.search.param" id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index fae0b3fd40..d50b008694 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -47,7 +47,7 @@ def get_folio(self, folio_id): amountTotal=round(folio.amount_total, 2), reservationType=folio.reservation_type, pendingAmount=folio.pending_amount, - lastCheckout=str(folio.last_checkout) + lastCheckout=str(folio.last_checkout), ) else: raise MissingError(_("Folio not found")) @@ -219,10 +219,7 @@ def get_folio_payments(self, folio_id, pms_search_param): ) def create_folio_charge(self, folio_id, pms_account_payment_info): folio = self.env["pms.folio"].browse(folio_id) - if pms_account_payment_info.partnerId: - partner_id = self.env["res.partner"].browse(int(pms_account_payment_info.partnerId)) - else: - partner_id = folio.partner_id + partner_id = self.env["res.partner"].browse(pms_account_payment_info.partnerId) journal_id = self.env["account.journal"].browse( pms_account_payment_info.journalId ) @@ -252,10 +249,7 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): ) def create_folio_refund(self, folio_id, pms_account_payment_info): folio = self.env["pms.folio"].browse(folio_id) - if pms_account_payment_info.partnerId: - partner_id = self.env["res.partner"].browse(pms_account_payment_info.partnerId) - else: - partner_id = folio.partner_id + partner_id = self.env["res.partner"].browse(pms_account_payment_info.partnerId) journal_id = self.env["account.journal"].browse( pms_account_payment_info.journalId ) @@ -269,6 +263,7 @@ def create_folio_refund(self, folio_id, pms_account_payment_info): services=False, partner=partner_id, date=datetime.strptime(pms_account_payment_info.date, "%m/%d/%Y"), + ref=pms_account_payment_info.reference, ) @restapi.method( From 29a6928df6af2428bb1a675961aa31fad829abe0 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 21 Sep 2022 12:40:52 +0200 Subject: [PATCH 167/547] [WIP]pms_api_rest: working in partner services --- pms_api_rest/datamodels/pms_partner.py | 1 - pms_api_rest/services/pms_partner_service.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index cad2afabb8..6aee0968f0 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -8,7 +8,6 @@ class PmsPartnerSearchParam(Datamodel): id = fields.Integer(required=False, allow_none=True) vat = fields.String(required=False, allow_none=True) - class PmsPartnerSearchParam(Datamodel): _name = "pms.partner.search.param" id = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 72a4958fe6..8e8f701333 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -43,6 +43,7 @@ def get_partners(self, pms_partner_search_params): 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() @@ -72,7 +73,6 @@ def get_partners(self, pms_partner_search_params): if partner.residence_country_id else None, tagIds=partner.category_id.ids if partner.category_id else [], - documentNumbers=partner.id_numbers if partner.id_numbers else [], vat=partner.vat if partner.vat else None, website=partner.website if partner.website else None, ) From bca3eaeaebd017fbaafb530cc3eebd1deae6f725 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 21 Sep 2022 16:48:53 +0200 Subject: [PATCH 168/547] [FIX]pms_api_rest: pre-commit conflict --- pms_api_rest/services/pms_price_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_price_service.py b/pms_api_rest/services/pms_price_service.py index a7a121896e..1ede7859b6 100644 --- a/pms_api_rest/services/pms_price_service.py +++ b/pms_api_rest/services/pms_price_service.py @@ -154,4 +154,4 @@ def _get_board_service_price( product_qty=product_qty, date_consumption=date_consumption, ) - return price + return From 85f2639f45490e797f9ea4c9273d38230f1b3afe Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 3 Oct 2022 19:42:12 +0200 Subject: [PATCH 169/547] [IMP]pms_api_rest: added get, post and patch services and datamdels to partner --- pms_api_rest/datamodels/__init__.py | 2 + .../datamodels/pms_account_payment_term.py | 9 + pms_api_rest/datamodels/pms_partner.py | 40 ++- pms_api_rest/datamodels/res_lang.py | 9 + pms_api_rest/services/__init__.py | 2 + .../pms_account_payment_terms_service.py | 35 +++ pms_api_rest/services/pms_partner_service.py | 280 ++++++++++++------ pms_api_rest/services/res_lang_service.py | 35 +++ 8 files changed, 309 insertions(+), 103 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_account_payment_term.py create mode 100644 pms_api_rest/datamodels/res_lang.py create mode 100644 pms_api_rest/services/pms_account_payment_terms_service.py create mode 100644 pms_api_rest/services/res_lang_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 981d688d63..0e29edbcc9 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -46,3 +46,5 @@ from . import pms_service from . import pms_service_line from . import res_users +from . import res_lang +from . import pms_account_payment_term 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..2bfd8d53de --- /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 PmsAccountPaymentTermInfo(Datamodel): + _name = "pms.account.payment.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_partner.py b/pms_api_rest/datamodels/pms_partner.py index 6aee0968f0..6db6d6e745 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -6,13 +6,9 @@ class PmsPartnerSearchParam(Datamodel): _name = "pms.partner.search.param" id = fields.Integer(required=False, allow_none=True) - vat = fields.String(required=False, allow_none=True) - -class PmsPartnerSearchParam(Datamodel): - _name = "pms.partner.search.param" - id = fields.Integer(required=False, allow_none=True) - vat = fields.String(required=False, allow_none=True) - name = fields.String(required=False, allow_none=True) + vatNumber = fields.String(required=False, allow_none=True) + documentType = fields.Integer(required=False, allow_none=True) + documentNumber = fields.String(required=False, allow_none=True) class PmsPartnerInfo(Datamodel): @@ -25,23 +21,39 @@ class PmsPartnerInfo(Datamodel): email = fields.String(required=False, allow_none=True) mobile = fields.String(required=False, allow_none=True) phone = fields.String(required=False, allow_none=True) - vat = 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) 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) - zip = fields.String(required=False, allow_none=True) + residenceStreet2 = fields.String(required=False, allow_none=True) + residenceZip = 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) + residenceStateId = fields.Integer(required=False, allow_none=True) isAgency = fields.Boolean(required=False, allow_none=True) - countryChar = fields.String(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) - countryName = fields.String(required=False, allow_none=True) + residenceCountryId = fields.Integer(required=False, allow_none=True) tagIds = fields.List(fields.Integer(required=False, allow_none=True)) - lastStay = fields.String(required=False, allow_none=True) website = fields.String(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) 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/services/__init__.py b/pms_api_rest/services/__init__.py index b2312c6b71..7a0fe80672 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -32,3 +32,5 @@ from . import pms_agency_service from . import pms_service_service from . import pms_service_line_service +from . import res_lang_service +from . import pms_account_payment_terms_service 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..14e4281087 --- /dev/null +++ b/pms_api_rest/services/pms_account_payment_terms_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 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.payment.term.info", is_list=True), + auth="jwt_api_pms", + ) + def get_account_payment_terms(self): + + PmsAccountPaymenttermInfo = self.env.datamodels["pms.account.payment.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_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 8e8f701333..3ee6d173e0 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -27,10 +27,6 @@ class PmsPartnerService(Component): def get_partners(self, pms_partner_search_params): result_partners = [] domain = [] - if pms_partner_search_params.vat: - domain.append(("vat", "=", pms_partner_search_params.vat)) - if pms_partner_search_params.name: - domain.append(("name", "ilike", pms_partner_search_params.name)) PmsPartnerInfo = self.env.datamodels["pms.partner.info"] for partner in self.env["res.partner"].search(domain): result_partners.append( @@ -49,37 +45,68 @@ def get_partners(self, pms_partner_search_params): ).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, - zip=partner.residence_zip if partner.residence_zip 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, - countryState=partner.residence_state_id.id + 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, - countryId=partner.residence_country_id.id - if partner.residence_country_id - else None, - countryChar=partner.residence_country_id.code_alpha3 - if partner.residence_country_id - else None, - countryName=partner.residence_country_id.name + isCompany=partner.is_company, + residenceCountryId=partner.residence_country_id.id if partner.residence_country_id else None, tagIds=partner.category_id.ids if partner.category_id else [], - vat=partner.vat if partner.vat else None, + vatNumber=partner.vat if partner.vat else None, + vatDocumentType=partner.vat_document_type + if partner.vat_document_type + else None, website=partner.website if partner.website 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, ) ) return result_partners - # REVIEW: analyze in which service file this method should be @restapi.method( [ ( @@ -148,106 +175,181 @@ def write_partner(self, partner_id, partner_info): } ) - # REVIEW: analyze in which service file this method should be @restapi.method( [ ( [ - "//", + "//deactivate", + ], + "PATCH", + ) + ], + auth="jwt_api_pms", + ) + def deactivate_partner(self, partner_id): + self.env["res.partner"].browse(partner_id).active = False + + @restapi.method( + [ + ( + [ + "//activate", + ], + "PATCH", + ) + ], + auth="jwt_api_pms", + ) + def activate_partner(self, partner_id): + self.env["res.partner"].browse(partner_id).active = True + + @restapi.method( + [ + ( + [ + "/partner", ], "GET", ) ], - output_param=Datamodel("pms.checkin.partner.info", is_list=True), + input_param=Datamodel("pms.partner.search.param", is_list=False), + output_param=Datamodel("pms.checkin.partner.info", is_list=False), auth="jwt_api_pms", ) - def get_partner_by_doc_number(self, document_type, document_number): - doc_number = self.env["res.partner.id_number"].search( - [("name", "=", document_number), ("category_id", "=", int(document_type))] - ) - partners = [] - PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] - if not doc_number: - pass - else: - if doc_number.valid_from: - document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") - if doc_number.partner_id.birthdate_date: - birthdate_date = doc_number.partner_id.birthdate_date.strftime( - "%d/%m/%Y" - ) - partners.append( - PmsCheckinPartnerInfo( - # id=doc_number.partner_id.id, - name=doc_number.partner_id.name - if doc_number.partner_id.name - else None, - firstname=doc_number.partner_id.firstname - if doc_number.partner_id.firstname - else None, - lastname=doc_number.partner_id.lastname - if doc_number.partner_id.lastname - else None, - lastname2=doc_number.partner_id.lastname2 - if doc_number.partner_id.lastname2 - else None, - email=doc_number.partner_id.email - if doc_number.partner_id.email - else None, - mobile=doc_number.partner_id.mobile - if doc_number.partner_id.mobile - else None, - documentType=int(document_type), - documentNumber=doc_number.name, - documentExpeditionDate=document_expedition_date - if doc_number.valid_from - else None, - documentSupportNumber=doc_number.support_number - if doc_number.support_number - else None, - gender=doc_number.partner_id.gender - if doc_number.partner_id.gender - else None, - birthdate=birthdate_date - if doc_number.partner_id.birthdate_date - else None, - residenceStreet=doc_number.partner_id.residence_street - if doc_number.partner_id.residence_street - else None, - zip=doc_number.partner_id.residence_zip - if doc_number.partner_id.residence_zip - else None, - residenceCity=doc_number.partner_id.residence_city - if doc_number.partner_id.residence_city - else None, - nationality=doc_number.partner_id.nationality_id.id - if doc_number.partner_id.nationality_id - else None, - countryState=doc_number.partner_id.residence_state_id.id - if doc_number.partner_id.residence_state_id - else None, - ) + def get_partner(self, pms_partner_search_params): + domain = [] + # if pms_partner_search_params.documentType + # and pms_partner_search_params.documentNumber: + # doc_number = self.env["res.partner.id_number"].search( + # [ + # ("name", "=", pms_partner_search_params.documentNumber), + # ("category_id", "=", pms_partner_search_params.documentType) + # ] + # ) + # if doc_number.valid_from: + # document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") + # document_type = pms_partner_search_params.documentType + # document_number = pms_partner_search_params.documentNumber + # domain.append(('id_numbers', 'in', doc_number)) + PmsPartnerInfo = self.env.datamodels["pms.partner.info"] + if pms_partner_search_params.vatNumber: + domain.append(("vat", "=", pms_partner_search_params.vatNumber)) + partner = self.env["res.partner"].search(domain) + if len(partner) > 1: + partner = partner.filtered("is_company") + if partner: + 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, + tagIds=partner.category_id.ids if partner.category_id else [], + vatNumber=partner.vat if partner.vat else None, + vatDocumentType=partner.vat_document_type + if partner.vat_document_type + else None, + website=partner.website if partner.website 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, ) - return partners + else: + return [] def mapping_partner_values(self, pms_partner_info): vals = dict() partner_fields = { + "name": pms_partner_info.name, "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.zip, "residence_city": pms_partner_info.residenceCity, - "residence_state_id": pms_partner_info.countryState, - "residence_country_id": pms_partner_info.nationality, + "residence_state_id": pms_partner_info.residenceStateId, + "residence_country_id": pms_partner_info.residenceCountryId, "is_agency": pms_partner_info.isAgency, - "vat": pms_partner_info.vat, - "website": pms_partner_info.website, + "is_company": pms_partner_info.isCompany, + "vat": pms_partner_info.vatNumber, + "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, } if pms_partner_info.birthdate: birthdate = datetime.strptime(pms_partner_info.birthdate, "%d/%m/%Y") 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 From c3693f71f5dd4806ce6bb35a9b822cccbd72ce9e Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 4 Oct 2022 18:13:05 +0200 Subject: [PATCH 170/547] [REF]pms_api_rest: change in get_partner domain and add invoice_month_day and invoice_to_agency fields --- pms_api_rest/datamodels/pms_partner.py | 4 +- pms_api_rest/services/pms_partner_service.py | 182 +++++++++++++------ 2 files changed, 127 insertions(+), 59 deletions(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index 6db6d6e745..136bf91105 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -43,8 +43,6 @@ class PmsPartnerInfo(Datamodel): stateId = fields.Integer(required=False, allow_none=True) countryId = fields.Integer(required=False, allow_none=True) residenceCountryId = fields.Integer(required=False, allow_none=True) - tagIds = fields.List(fields.Integer(required=False, allow_none=True)) - website = fields.String(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) @@ -57,3 +55,5 @@ class PmsPartnerInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 3ee6d173e0..9be1ad9bb5 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -75,12 +75,10 @@ def get_partners(self, pms_partner_search_params): residenceCountryId=partner.residence_country_id.id if partner.residence_country_id else None, - tagIds=partner.category_id.ids if partner.category_id else [], vatNumber=partner.vat if partner.vat else None, vatDocumentType=partner.vat_document_type if partner.vat_document_type else None, - website=partner.website if partner.website 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, @@ -103,6 +101,12 @@ def get_partners(self, pms_partner_search_params): 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, ) ) return result_partners @@ -121,18 +125,7 @@ def get_partners(self, pms_partner_search_params): ) def create_partner(self, partner_info): vals = self.mapping_partner_values(partner_info) - partner = self.env["res.partner"].create(vals) - if partner_info.documentNumber: - doc_type_id = ( - self.env["res.partner.id_category"].search([("name", "=", "DNI")]).id - ) - self.env["res.partner.id_number"].create( - { - "partner_id": partner.id, - "category_id": doc_type_id, - "name": partner_info.documentNumber, - } - ) + self.env["res.partner"].create(vals) @restapi.method( [ @@ -150,30 +143,88 @@ 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)) - if partner_info.documentNumber: - doc_type_id = ( - self.env["res.partner.id_category"].search([("name", "=", "DNI")]).id - ) - doc_number = self.env["res.partner.id_number"].search( + + # REVIEW: analyze in which service file this method should be + @restapi.method( + [ + ( [ - ("partner_id", "=", partner_id), - ("name", "=", partner_info.documentNumber), - ("category_id", "=", doc_type_id), - ] + "//", + ], + "GET", ) - if not doc_number: - self.env["res.partner.id_number"].create( - { - "category_id": doc_type_id, - "name": partner_info.documentNumber, - } + ], + output_param=Datamodel("pms.checkin.partner.info", is_list=True), + auth="jwt_api_pms", + ) + def get_partner_by_doc_number(self, document_type, document_number): + doc_number = self.env["res.partner.id_number"].search( + [("name", "=", document_number), ("category_id", "=", int(document_type))] + ) + partners = [] + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] + if not doc_number: + pass + else: + if doc_number.valid_from: + document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") + if doc_number.partner_id.birthdate_date: + birthdate_date = doc_number.partner_id.birthdate_date.strftime( + "%d/%m/%Y" ) - else: - doc_number.write( - { - "name": partner_info.documentNumber, - } + partners.append( + PmsCheckinPartnerInfo( + # id=doc_number.partner_id.id, + name=doc_number.partner_id.name + if doc_number.partner_id.name + else None, + firstname=doc_number.partner_id.firstname + if doc_number.partner_id.firstname + else None, + lastname=doc_number.partner_id.lastname + if doc_number.partner_id.lastname + else None, + lastname2=doc_number.partner_id.lastname2 + if doc_number.partner_id.lastname2 + else None, + email=doc_number.partner_id.email + if doc_number.partner_id.email + else None, + mobile=doc_number.partner_id.mobile + if doc_number.partner_id.mobile + else None, + documentType=int(document_type), + documentNumber=doc_number.name, + documentExpeditionDate=document_expedition_date + if doc_number.valid_from + else None, + documentSupportNumber=doc_number.support_number + if doc_number.support_number + else None, + gender=doc_number.partner_id.gender + if doc_number.partner_id.gender + else None, + birthdate=birthdate_date + if doc_number.partner_id.birthdate_date + else None, + residenceStreet=doc_number.partner_id.residence_street + if doc_number.partner_id.residence_street + else None, + zip=doc_number.partner_id.residence_zip + if doc_number.partner_id.residence_zip + else None, + residenceCity=doc_number.partner_id.residence_city + if doc_number.partner_id.residence_city + else None, + nationality=doc_number.partner_id.nationality_id.id + if doc_number.partner_id.nationality_id + else None, + countryState=doc_number.partner_id.residence_state_id.id + if doc_number.partner_id.residence_state_id + else None, ) + ) + return partners @restapi.method( [ @@ -213,31 +264,22 @@ def activate_partner(self, partner_id): ) ], input_param=Datamodel("pms.partner.search.param", is_list=False), - output_param=Datamodel("pms.checkin.partner.info", is_list=False), + output_param=Datamodel("pms.partner.info", is_list=False), auth="jwt_api_pms", ) def get_partner(self, pms_partner_search_params): domain = [] - # if pms_partner_search_params.documentType - # and pms_partner_search_params.documentNumber: - # doc_number = self.env["res.partner.id_number"].search( - # [ - # ("name", "=", pms_partner_search_params.documentNumber), - # ("category_id", "=", pms_partner_search_params.documentType) - # ] - # ) - # if doc_number.valid_from: - # document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") - # document_type = pms_partner_search_params.documentType - # document_number = pms_partner_search_params.documentNumber - # domain.append(('id_numbers', 'in', doc_number)) PmsPartnerInfo = self.env.datamodels["pms.partner.info"] if pms_partner_search_params.vatNumber: - domain.append(("vat", "=", pms_partner_search_params.vatNumber)) + domain = [ + "|", + ("vat", "=", pms_partner_search_params.vatNumber), + ("aeat_identification", "=", pms_partner_search_params.vatNumber), + ] partner = self.env["res.partner"].search(domain) - if len(partner) > 1: - partner = partner.filtered("is_company") - if partner: + if not partner: + return PmsPartnerInfo() + else: return PmsPartnerInfo( id=partner.id, name=partner.name if partner.name else None, @@ -281,12 +323,10 @@ def get_partner(self, pms_partner_search_params): residenceCountryId=partner.residence_country_id.id if partner.residence_country_id else None, - tagIds=partner.category_id.ids if partner.category_id else [], vatNumber=partner.vat if partner.vat else None, vatDocumentType=partner.vat_document_type if partner.vat_document_type else None, - website=partner.website if partner.website 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, @@ -309,9 +349,13 @@ def get_partner(self, pms_partner_search_params): 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, ) - else: - return [] def mapping_partner_values(self, pms_partner_info): vals = dict() @@ -333,7 +377,6 @@ def mapping_partner_values(self, pms_partner_info): "residence_country_id": pms_partner_info.residenceCountryId, "is_agency": pms_partner_info.isAgency, "is_company": pms_partner_info.isCompany, - "vat": pms_partner_info.vatNumber, "street": pms_partner_info.street, "street2": pms_partner_info.street2, "zip": pms_partner_info.zip, @@ -350,7 +393,32 @@ def mapping_partner_values(self, pms_partner_info): "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, + "vat_document_type": "vat", + } + ) + 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") From 2b368ee4e63b221dfc91b9d661df414ef013f941 Mon Sep 17 00:00:00 2001 From: Sara Date: Fri, 19 Aug 2022 12:27:32 +0200 Subject: [PATCH 171/547] [RFC]pms_api_rest: rename segmentations service for partner categories --- .../services/res_partner_category_service.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pms_api_rest/services/res_partner_category_service.py b/pms_api_rest/services/res_partner_category_service.py index b24dfdf1a7..76b4160bc0 100644 --- a/pms_api_rest/services/res_partner_category_service.py +++ b/pms_api_rest/services/res_partner_category_service.py @@ -6,7 +6,7 @@ class PmsPartnerCategoriesService(Component): _inherit = "base.rest.service" _name = "res.partner.category.service" - _usage = "segmentations" + _usage = "categories" _collection = "pms.services" @restapi.method( @@ -21,17 +21,17 @@ class PmsPartnerCategoriesService(Component): output_param=Datamodel("res.partner.category.info", is_list=True), auth="jwt_api_pms", ) - def get_parent_segmentation_ids(self): - result_segmentation_ids = [] + def get_categories(self): + result_categories = [] ResPartnerCategoryInfo = self.env.datamodels["res.partner.category.info"] - for segmentation_id in self.env["res.partner.category"].search([]): - result_segmentation_ids.append( + for category in self.env["res.partner.category"].search([]): + result_categories.append( ResPartnerCategoryInfo( - id=segmentation_id.id, - name=segmentation_id.name, - parentId=segmentation_id.parent_id.id - if segmentation_id.parent_id.id + id=category.id, + name=category.name, + parentId=category.parent_id.id + if category.parent_id.id else 0, ) ) - return result_segmentation_ids + return result_categories From b68412b8763dbc6aa1f9719e1695fecb64c16f45 Mon Sep 17 00:00:00 2001 From: Sara Date: Tue, 23 Aug 2022 12:17:35 +0200 Subject: [PATCH 172/547] [IMP] pms_api_rest: add partner datamodel and service --- pms_api_rest/datamodels/pms_partner.py | 2 ++ pms_api_rest/services/pms_partner_service.py | 7 ++++--- pms_api_rest/services/res_partner_category_service.py | 4 +--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index 136bf91105..cafb455efe 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -57,3 +57,5 @@ class PmsPartnerInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 9be1ad9bb5..4b67d3baf0 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -107,6 +107,7 @@ def get_partners(self, pms_partner_search_params): invoiceToAgency=partner.invoice_to_agency if partner.invoice_to_agency else None, + tagIds=partner.category_id.ids if partner.category_id else [], ) ) return result_partners @@ -154,7 +155,7 @@ def write_partner(self, partner_id, partner_info): "GET", ) ], - output_param=Datamodel("pms.checkin.partner.info", is_list=True), + output_param=Datamodel("pms.partner.info", is_list=True), auth="jwt_api_pms", ) def get_partner_by_doc_number(self, document_type, document_number): @@ -162,7 +163,7 @@ def get_partner_by_doc_number(self, document_type, document_number): [("name", "=", document_number), ("category_id", "=", int(document_type))] ) partners = [] - PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] + PmsPartnerInfo = self.env.datamodels["pms.partner.info"] if not doc_number: pass else: @@ -173,7 +174,7 @@ def get_partner_by_doc_number(self, document_type, document_number): "%d/%m/%Y" ) partners.append( - PmsCheckinPartnerInfo( + PmsPartnerInfo( # id=doc_number.partner_id.id, name=doc_number.partner_id.name if doc_number.partner_id.name diff --git a/pms_api_rest/services/res_partner_category_service.py b/pms_api_rest/services/res_partner_category_service.py index 76b4160bc0..34f8af8d45 100644 --- a/pms_api_rest/services/res_partner_category_service.py +++ b/pms_api_rest/services/res_partner_category_service.py @@ -29,9 +29,7 @@ def get_categories(self): ResPartnerCategoryInfo( id=category.id, name=category.name, - parentId=category.parent_id.id - if category.parent_id.id - else 0, + parentId=category.parent_id.id if category.parent_id.id else 0, ) ) return result_categories From a6f962f4db8e0dd657a9f0909d27f40058da03f5 Mon Sep 17 00:00:00 2001 From: Sara Date: Thu, 8 Sep 2022 13:12:32 +0200 Subject: [PATCH 173/547] [IMP] pms_api_rest: add service for partner reservations as host and as customer, update partner datamodel --- pms_api_rest/datamodels/pms_partner.py | 2 +- pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/datamodels/res_country.py | 1 + pms_api_rest/services/pms_partner_service.py | 97 +++++++++++++++++++- pms_api_rest/services/res_country_service.py | 1 + 5 files changed, 100 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index cafb455efe..c2e509d42c 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -30,8 +30,8 @@ class PmsPartnerInfo(Datamodel): age = fields.Integer(required=False, allow_none=True) residenceStreet = fields.String(required=False, allow_none=True) residenceStreet2 = fields.String(required=False, allow_none=True) - residenceZip = 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) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 5808b0842c..6f4e9aefbb 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -16,6 +16,7 @@ class PmsReservationShortInfo(Datamodel): stateCode = fields.String(required=False, allow_none=True) stateDescription = fields.String(required=False, allow_none=True) children = fields.Integer(required=False, allow_none=True) + paymentStateDescription = fields.String(required=False, allow_none=True) readyForCheckin = fields.Boolean(required=False, allow_none=True) allowedCheckout = fields.Boolean(required=False, allow_none=True) splitted = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/res_country.py b/pms_api_rest/datamodels/res_country.py index d584be8c2b..ac4921d823 100644 --- a/pms_api_rest/datamodels/res_country.py +++ b/pms_api_rest/datamodels/res_country.py @@ -7,6 +7,7 @@ class PmsResCountriesInfo(Datamodel): _name = "res.country.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) + alpha3 = fields.String(required=False, allow_none=True) class PmsResCountryStatesInfo(Datamodel): diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 4b67d3baf0..5ea59ef0b9 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -29,6 +29,11 @@ def get_partners(self, pms_partner_search_params): domain = [] PmsPartnerInfo = self.env.datamodels["pms.partner.info"] for partner in self.env["res.partner"].search(domain): + checkouts = ( + self.env["pms.checkin.partner"] + .search([("partner_id.id", "=", partner.id)]) + .mapped("checkout") + ) result_partners.append( PmsPartnerInfo( id=partner.id, @@ -37,7 +42,6 @@ def get_partners(self, pms_partner_search_params): 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( @@ -107,7 +111,10 @@ def get_partners(self, pms_partner_search_params): invoiceToAgency=partner.invoice_to_agency if partner.invoice_to_agency else None, + agencyStateId=partner.state_id.id if partner.state_id else None, + agencyCity=partner.city if partner.city else None, tagIds=partner.category_id.ids if partner.category_id else [], + lastStay=max(checkouts).strftime("%d/%m/%Y") if checkouts else "", ) ) return result_partners @@ -128,6 +135,51 @@ def create_partner(self, partner_info): vals = self.mapping_partner_values(partner_info) self.env["res.partner"].create(vals) + + @restapi.method( + [ + ( + [ "//hosted-reservations", + ], + "GET", + ) + ], + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="jwt_api_pms", + ) + def get_partner_as_host(self, partner_id): + checkins = self.env["pms.checkin.partner"].search( + [("partner_id", "=", partner_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)] + ) + folio = self.env["pms.folio"].search( + [("id", "=", reservation.folio_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), + stateDescription=dict( + reservation.fields_get(["state"])["state"]["selection"] + )[reservation.state], + paymentStateDescription=dict( + folio.fields_get(["payment_state"])["payment_state"][ + "selection" + ] + )[folio.payment_state], + ) + ) + return reservations + @restapi.method( [ ( @@ -146,6 +198,49 @@ def write_partner(self, partner_id, partner_info): partner.write(self.mapping_partner_values(partner_info)) # REVIEW: analyze in which service file this method should be + + + @restapi.method( + [ + ( + [ "//customer-reservations", + ], + "GET", + ) + ], + output_param=Datamodel("pms.reservation.short.info", is_list=True), + auth="jwt_api_pms", + ) + def get_partner_as_customer(self, partner_id): + partnerReservations = self.env["pms.reservation"].search( + [("partner_id", "=", partner_id)] + ) + PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] + reservations = [] + for reservation in partnerReservations: + folio = self.env["pms.folio"].search([("id", "=", reservation.folio_id.id)]) + reservations.append( + PmsReservationShortInfo( + checkin=datetime.combine( + reservation.checkin, datetime.min.time() + ).isoformat(), + checkout=datetime.combine( + reservation.checkout, datetime.min.time() + ).isoformat(), + adults=reservation.adults, + priceTotal=round(reservation.price_room_services_set, 2), + stateDescription=dict( + reservation.fields_get(["state"])["state"]["selection"] + )[reservation.state], + paymentStateDescription=dict( + folio.fields_get(["payment_state"])["payment_state"][ + "selection" + ] + )[folio.payment_state], + ) + ) + return reservations + @restapi.method( [ ( diff --git a/pms_api_rest/services/res_country_service.py b/pms_api_rest/services/res_country_service.py index d8bb1c2eea..c34f0e4b00 100644 --- a/pms_api_rest/services/res_country_service.py +++ b/pms_api_rest/services/res_country_service.py @@ -29,6 +29,7 @@ def get_countries(self): ResCountriesInfo( id=country.id, name=country.name, + alpha3=country.code_alpha3 if country.code_alpha3 else None, ) ) return result_countries From 80118feb914129b0bd62342e002857319e5ac207 Mon Sep 17 00:00:00 2001 From: Sara Date: Tue, 13 Sep 2022 14:49:14 +0200 Subject: [PATCH 174/547] [IMP] pms_api_rest: add account_journals service and remove get_payment_methods in properties service --- .../datamodels/pms_account_journal.py | 5 ++ pms_api_rest/services/__init__.py | 2 + .../services/pms_account_journal_service.py | 51 +++++++++++++++++++ pms_api_rest/services/pms_property_service.py | 33 ------------ 4 files changed, 58 insertions(+), 33 deletions(-) create mode 100644 pms_api_rest/services/pms_account_journal_service.py diff --git a/pms_api_rest/datamodels/pms_account_journal.py b/pms_api_rest/datamodels/pms_account_journal.py index 6c96239bd2..76a076b048 100644 --- a/pms_api_rest/datamodels/pms_account_journal.py +++ b/pms_api_rest/datamodels/pms_account_journal.py @@ -3,6 +3,11 @@ 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) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 7a0fe80672..6017095d1c 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -32,5 +32,7 @@ from . import pms_agency_service from . import pms_service_service from . import pms_service_line_service + from . import res_lang_service from . import pms_account_payment_terms_service +from . import pms_account_journal_service 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..d39215bf64 --- /dev/null +++ b/pms_api_rest/services/pms_account_journal_service.py @@ -0,0 +1,51 @@ +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): + domain = [] + if account_journal_search_param.pmsPropertyId: + domain.extend( + [ + "|", + ( + "pms_property_ids", + "in", + account_journal_search_param.pmsPropertyId, + ), + ("pms_property_ids", "=", False), + ] + ) + PmsAccountJournalInfo = self.env.datamodels["pms.account.journal.info"] + result_account_journals = [] + for account_journal in self.env["account.journal"].search( + domain, + ): + result_account_journals.append( + PmsAccountJournalInfo( + id=account_journal.id, + name=account_journal.name, + allowedPayments=account_journal.allowed_pms_payments, + ) + ) + return result_account_journals diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index d889e3237c..8b00af54d7 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -88,39 +88,6 @@ def get_property(self, property_id): return res - @restapi.method( - [ - ( - [ - "//payment-methods", - ], - "GET", - ) - ], - output_param=Datamodel("pms.account.journal.info", is_list=True), - auth="jwt_api_pms", - ) - def get_method_payments_property(self, property_id): - - pms_property = self.env["pms.property"].search([("id", "=", property_id)]) - PmsAccountJournalInfo = self.env.datamodels["pms.account.journal.info"] - res = [] - if not pms_property: - pass - else: - for method in pms_property._get_payment_methods(automatic_included=True): - payment_method = self.env["account.journal"].search( - [("id", "=", method.id)] - ) - res.append( - PmsAccountJournalInfo( - id=payment_method.id, - name=payment_method.name, - allowedPayments=payment_method.allowed_pms_payments, - ) - ) - return res - @restapi.method( [ ( From 2b5fcf476f76e69cdc42a47f6f82fc1eee75b7a2 Mon Sep 17 00:00:00 2001 From: Sara Date: Tue, 13 Sep 2022 14:54:28 +0200 Subject: [PATCH 175/547] [IMP] pms_api_rest: add get for payments and invoices in partner service --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_account_move.py | 13 ++++ pms_api_rest/datamodels/pms_payment.py | 12 ++++ pms_api_rest/services/pms_partner_service.py | 63 ++++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_account_move.py create mode 100644 pms_api_rest/datamodels/pms_payment.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 0e29edbcc9..5e0a0d70aa 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -15,6 +15,7 @@ from . import pms_property from . import pms_account_journal from . import pms_account_payment +from . import pms_account_move from . import pms_user diff --git a/pms_api_rest/datamodels/pms_account_move.py b/pms_api_rest/datamodels/pms_account_move.py new file mode 100644 index 0000000000..8b47fe8819 --- /dev/null +++ b/pms_api_rest/datamodels/pms_account_move.py @@ -0,0 +1,13 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsAccountMove(Datamodel): + _name = "pms.account.move.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) diff --git a/pms_api_rest/datamodels/pms_payment.py b/pms_api_rest/datamodels/pms_payment.py new file mode 100644 index 0000000000..ade3dcbcc7 --- /dev/null +++ b/pms_api_rest/datamodels/pms_payment.py @@ -0,0 +1,12 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPaymentInfo(Datamodel): + _name = "pms.payment.info" + id = fields.Integer(required=False, allow_none=True) + amount = fields.Float(required=False, allow_none=True) + journalId = fields.Integer(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + memo = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 5ea59ef0b9..e955c6847a 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -241,6 +241,69 @@ def get_partner_as_customer(self, partner_id): ) return reservations + @restapi.method( + [ + ( + [ + "//payments", + ], + "GET", + ) + ], + output_param=Datamodel("pms.payment.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)] + ) + PmsPaymentInfo = self.env.datamodels["pms.payment.info"] + payments = [] + for payment in partnerPayments: + payments.append( + PmsPaymentInfo( + id=payment.id, + amount=round(payment.amount, 2), + journalId=payment.journal_id.id, + date=payment.date.strftime("%d/%m/%Y"), + memo=payment.ref, + ) + ) + return payments + + @restapi.method( + [ + ( + [ + "//invoices", + ], + "GET", + ) + ], + output_param=Datamodel("pms.account.move.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)] + ) + PmsAcoountMoveInfo = self.env.datamodels["pms.account.move.info"] + invoices = [] + for invoice in partnerInvoices: + invoices.append( + PmsAcoountMoveInfo( + id=invoice.id, + name=invoice.name, + amount=round(invoice.amount_total, 2), + date=invoice.date.strftime("%d/%m/%Y"), + state=invoice.state, + paymentState=invoice.payment_state + if invoice.payment_state + else None, + ) + ) + return invoices + @restapi.method( [ ( From d20929dd59d7be9fb61415201cec56f232537825 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 15 Sep 2022 11:49:39 +0200 Subject: [PATCH 176/547] [IMP] pms_api_rest: modified the domain in get_partner_invoices --- pms_api_rest/services/pms_partner_service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index e955c6847a..e7c3cf1916 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -285,7 +285,10 @@ def get_partner_payments(self, partner_id): ) def get_partner_invoices(self, partner_id): partnerInvoices = self.env["account.move"].search( - [("partner_id", "=", partner_id)] + [ + ("partner_id", "=", partner_id), + ("move_type", "in", self.env["account.move"].get_invoice_types()), + ] ) PmsAcoountMoveInfo = self.env.datamodels["pms.account.move.info"] invoices = [] From 66cec3c78abcaf2ab7af96c8d338eee44b5c537e Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 16 Sep 2022 09:11:36 +0200 Subject: [PATCH 177/547] [IMP] pms_api_rest: correct and unify fields in partner datamodel --- pms_api_rest/services/pms_partner_service.py | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index e7c3cf1916..c8b038f00e 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -50,6 +50,7 @@ def get_partners(self, pms_partner_search_params): if partner.birthdate_date else None, age=partner.age if partner.age else None, + mobile=str(partner.mobile), residenceStreet=partner.residence_street if partner.residence_street else None, @@ -135,11 +136,11 @@ def create_partner(self, partner_info): vals = self.mapping_partner_values(partner_info) self.env["res.partner"].create(vals) - @restapi.method( - [ - ( - [ "//hosted-reservations", + [ + ( + [ + "//hosted-reservations", ], "GET", ) @@ -199,11 +200,11 @@ def write_partner(self, partner_id, partner_info): # REVIEW: analyze in which service file this method should be - @restapi.method( - [ - ( - [ "//customer-reservations", + [ + ( + [ + "//customer-reservations", ], "GET", ) @@ -372,7 +373,7 @@ def get_partner_by_doc_number(self, document_type, document_number): residenceStreet=doc_number.partner_id.residence_street if doc_number.partner_id.residence_street else None, - zip=doc_number.partner_id.residence_zip + residenceZip=doc_number.partner_id.residence_zip if doc_number.partner_id.residence_zip else None, residenceCity=doc_number.partner_id.residence_city @@ -533,7 +534,7 @@ def mapping_partner_values(self, pms_partner_info): "residence_street": pms_partner_info.residenceStreet, "residence_street2": pms_partner_info.residenceStreet2, "nationality_id": pms_partner_info.nationality, - "residence_zip": pms_partner_info.zip, + "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, From e288cf91c6951f5228cd46cb149c3c2026d4d524 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 21 Sep 2022 17:16:01 +0200 Subject: [PATCH 178/547] [RFC] pms_api_rest: rename reservation datamodel fields --- pms_api_rest/datamodels/pms_reservation.py | 2 +- pms_api_rest/datamodels/res_country.py | 2 +- pms_api_rest/services/pms_partner_service.py | 26 +++++--------------- pms_api_rest/services/res_country_service.py | 2 +- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 6f4e9aefbb..2bf69abe3a 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -16,7 +16,7 @@ class PmsReservationShortInfo(Datamodel): stateCode = fields.String(required=False, allow_none=True) stateDescription = fields.String(required=False, allow_none=True) children = fields.Integer(required=False, allow_none=True) - paymentStateDescription = fields.String(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) splitted = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/res_country.py b/pms_api_rest/datamodels/res_country.py index ac4921d823..f8b8fdbf14 100644 --- a/pms_api_rest/datamodels/res_country.py +++ b/pms_api_rest/datamodels/res_country.py @@ -7,7 +7,7 @@ class PmsResCountriesInfo(Datamodel): _name = "res.country.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - alpha3 = fields.String(required=False, allow_none=True) + code = fields.String(required=False, allow_none=True) class PmsResCountryStatesInfo(Datamodel): diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index c8b038f00e..d77b653ccc 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -159,9 +159,7 @@ def get_partner_as_host(self, partner_id): reservation = self.env["pms.reservation"].search( [("id", "=", checkin.reservation_id.id)] ) - folio = self.env["pms.folio"].search( - [("id", "=", reservation.folio_id.id)] - ) + reservations.append( PmsReservationShortInfo( id=reservation.id, @@ -169,14 +167,8 @@ def get_partner_as_host(self, partner_id): checkout=reservation.checkout.strftime("%d/%m/%Y"), adults=reservation.adults, priceTotal=round(reservation.price_room_services_set, 2), - stateDescription=dict( - reservation.fields_get(["state"])["state"]["selection"] - )[reservation.state], - paymentStateDescription=dict( - folio.fields_get(["payment_state"])["payment_state"][ - "selection" - ] - )[folio.payment_state], + stateCode=reservation.state, + paymentState=reservation.folio_payment_state, ) ) return reservations @@ -219,7 +211,6 @@ def get_partner_as_customer(self, partner_id): PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] reservations = [] for reservation in partnerReservations: - folio = self.env["pms.folio"].search([("id", "=", reservation.folio_id.id)]) reservations.append( PmsReservationShortInfo( checkin=datetime.combine( @@ -230,14 +221,8 @@ def get_partner_as_customer(self, partner_id): ).isoformat(), adults=reservation.adults, priceTotal=round(reservation.price_room_services_set, 2), - stateDescription=dict( - reservation.fields_get(["state"])["state"]["selection"] - )[reservation.state], - paymentStateDescription=dict( - folio.fields_get(["payment_state"])["payment_state"][ - "selection" - ] - )[folio.payment_state], + stateCode=reservation.state, + paymentState=reservation.folio_payment_state, ) ) return reservations @@ -382,6 +367,7 @@ def get_partner_by_doc_number(self, document_type, document_number): nationality=doc_number.partner_id.nationality_id.id if doc_number.partner_id.nationality_id else None, + # aquí tiene que ser id countryState=doc_number.partner_id.residence_state_id.id if doc_number.partner_id.residence_state_id else None, diff --git a/pms_api_rest/services/res_country_service.py b/pms_api_rest/services/res_country_service.py index c34f0e4b00..ea6d7b194e 100644 --- a/pms_api_rest/services/res_country_service.py +++ b/pms_api_rest/services/res_country_service.py @@ -29,7 +29,7 @@ def get_countries(self): ResCountriesInfo( id=country.id, name=country.name, - alpha3=country.code_alpha3 if country.code_alpha3 else None, + code=country.code if country.code else None, ) ) return result_countries From 93a4a62ef6449f3acfa9abdf19a019453e6d0f62 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 30 Sep 2022 14:37:59 +0200 Subject: [PATCH 179/547] [RFC] pms_api_rest: change partner reservations from partner service to reservation service --- pms_api_rest/services/pms_partner_service.py | 74 ------------------- .../services/pms_reservation_service.py | 70 ++++++++++++++++++ 2 files changed, 70 insertions(+), 74 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index d77b653ccc..661ea38706 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -136,43 +136,6 @@ def create_partner(self, partner_info): vals = self.mapping_partner_values(partner_info) self.env["res.partner"].create(vals) - @restapi.method( - [ - ( - [ - "//hosted-reservations", - ], - "GET", - ) - ], - output_param=Datamodel("pms.reservation.short.info", is_list=True), - auth="jwt_api_pms", - ) - def get_partner_as_host(self, partner_id): - checkins = self.env["pms.checkin.partner"].search( - [("partner_id", "=", partner_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( [ ( @@ -190,43 +153,6 @@ def write_partner(self, partner_id, partner_info): if partner: partner.write(self.mapping_partner_values(partner_info)) - # REVIEW: analyze in which service file this method should be - - @restapi.method( - [ - ( - [ - "//customer-reservations", - ], - "GET", - ) - ], - output_param=Datamodel("pms.reservation.short.info", is_list=True), - auth="jwt_api_pms", - ) - def get_partner_as_customer(self, partner_id): - partnerReservations = self.env["pms.reservation"].search( - [("partner_id", "=", partner_id)] - ) - PmsReservationShortInfo = self.env.datamodels["pms.reservation.short.info"] - reservations = [] - for reservation in partnerReservations: - reservations.append( - PmsReservationShortInfo( - checkin=datetime.combine( - reservation.checkin, datetime.min.time() - ).isoformat(), - checkout=datetime.combine( - reservation.checkout, datetime.min.time() - ).isoformat(), - adults=reservation.adults, - priceTotal=round(reservation.price_room_services_set, 2), - stateCode=reservation.state, - paymentState=reservation.folio_payment_state, - ) - ) - return reservations - @restapi.method( [ ( diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 7671bccb2b..0fa9ada714 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -536,6 +536,76 @@ def write_reservation_checkin_partner( self.mapping_checkin_partner_values(pms_checkin_partner_info) ) + @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( [ ( From 33ddd924ca8406ca17238779940d84f601df5f4c Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 30 Sep 2022 14:41:50 +0200 Subject: [PATCH 180/547] [IMP] pms_api_rest: add fields for pagination on their corresponding interfaces and modify get_partners service --- pms_api_rest/datamodels/__init__.py | 2 +- pms_api_rest/datamodels/pms_partner.py | 8 ++++++++ pms_api_rest/datamodels/pms_rest_metadata.py | 10 ++++++++++ pms_api_rest/services/pms_partner_service.py | 16 +++++++++++++--- 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_rest_metadata.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 5e0a0d70aa..4d649c1704 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -8,7 +8,7 @@ from . import pms_reservation from . import pms_reservation_line - +from . import pms_rest_metadata from . import pms_checkin_partner from . import pms_partner diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index c2e509d42c..7701993222 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -1,10 +1,12 @@ 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) vatNumber = fields.String(required=False, allow_none=True) documentType = fields.Integer(required=False, allow_none=True) @@ -59,3 +61,9 @@ class PmsPartnerInfo(Datamodel): 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_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/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 661ea38706..57b7a09439 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -21,14 +21,21 @@ class PmsPartnerService(Component): ) ], input_param=Datamodel("pms.partner.search.param", is_list=False), - output_param=Datamodel("pms.partner.info", is_list=True), + output_param=Datamodel("pms.partner.results", is_list=False), auth="jwt_api_pms", ) def get_partners(self, pms_partner_search_params): result_partners = [] domain = [] + PmsPartnerResults = self.env.datamodels["pms.partner.results"] PmsPartnerInfo = self.env.datamodels["pms.partner.info"] - for partner in self.env["res.partner"].search(domain): + total_partners = self.env["res.partner"].search_count(domain) + for partner in self.env["res.partner"].search( + domain, + order=pms_partner_search_params.orderBy, + limit=pms_partner_search_params.limit, + offset=pms_partner_search_params.offset + ): checkouts = ( self.env["pms.checkin.partner"] .search([("partner_id.id", "=", partner.id)]) @@ -118,7 +125,10 @@ def get_partners(self, pms_partner_search_params): lastStay=max(checkouts).strftime("%d/%m/%Y") if checkouts else "", ) ) - return result_partners + return PmsPartnerResults( + partners=result_partners, + total=total_partners + ) @restapi.method( [ From f9f04e532270e1342214cbb2eed56bb8d96aec58 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 5 Oct 2022 16:04:45 +0200 Subject: [PATCH 181/547] [IMP] pms_api_rest: add filters in partner service --- pms_api_rest/datamodels/pms_partner.py | 4 +++- pms_api_rest/services/pms_partner_service.py | 22 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index 7701993222..e1765fb660 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -8,9 +8,11 @@ class PmsPartnerSearchParam(Datamodel): _name = "pms.partner.search.param" _inherit = "pms.rest.metadata" id = fields.Integer(required=False, allow_none=True) - vatNumber = fields.String(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) + housed = fields.Boolean(required=False, allow_none=True) + filter = fields.String(required=False, allow_none=True) class PmsPartnerInfo(Datamodel): diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 57b7a09439..2771a19af6 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -3,6 +3,7 @@ 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.odoo.osv import expression class PmsPartnerService(Component): @@ -26,7 +27,28 @@ class PmsPartnerService(Component): ) def get_partners(self, pms_partner_search_params): result_partners = [] + domain_fields = [] + + if pms_partner_search_params.name: + domain_fields.append(("name", "ilike", pms_partner_search_params.name)) + if pms_partner_search_params.housed: + partners_housed = self.env["pms.checkin.partner"].search([("state", "=", "onboard")]).mapped( + "partner_id") + domain_fields.append(("id", "in", partners_housed.ids)) + domain_filter = list() + if pms_partner_search_params.filter: + for search in pms_partner_search_params.filter.split(" "): + subdomains = [ + [("name", "ilike", search)], + [("firstname", "ilike", search)], + [("lastname", "ilike", search)], + ] + domain_filter.append(expression.OR(subdomains)) domain = [] + if domain_filter: + domain = expression.AND([domain_fields, domain_filter[0]]) + else: + domain = domain_fields PmsPartnerResults = self.env.datamodels["pms.partner.results"] PmsPartnerInfo = self.env.datamodels["pms.partner.info"] total_partners = self.env["res.partner"].search_count(domain) From 10253a6b404113d004c634fe7433e34756ec9e7e Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 5 Oct 2022 18:25:25 +0200 Subject: [PATCH 182/547] [WIP] pms_api_rest: rebase --- pms_api_rest/__manifest__.py | 1 + .../datamodels/pms_account_payment.py | 1 + pms_api_rest/datamodels/pms_partner.py | 1 + pms_api_rest/datamodels/pms_payment.py | 12 ------ .../services/pms_account_journal_service.py | 39 ++++++++----------- pms_api_rest/services/pms_partner_service.py | 23 ++++++----- 6 files changed, 31 insertions(+), 46 deletions(-) delete mode 100644 pms_api_rest/datamodels/pms_payment.py diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 0664236b56..c55bf76834 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -13,6 +13,7 @@ "auth_signup", "auth_jwt_login", "base_location", + "l10n_es_aeat", ], "external_dependencies": { "python": ["jwt", "simplejson", "marshmallow", "jose"], diff --git a/pms_api_rest/datamodels/pms_account_payment.py b/pms_api_rest/datamodels/pms_account_payment.py index 5723e3d8c3..45025b669d 100644 --- a/pms_api_rest/datamodels/pms_account_payment.py +++ b/pms_api_rest/datamodels/pms_account_payment.py @@ -10,6 +10,7 @@ class PmsPaymentInfo(Datamodel): journalId = fields.Integer(required=False, allow_none=True) date = fields.String(required=False, allow_none=True) paymentType = fields.String(required=False, allow_none=True) + reference = fields.String(required=False, allow_none=True) class PmsAccountPaymentInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index e1765fb660..ae8078c214 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -8,6 +8,7 @@ class PmsPartnerSearchParam(Datamodel): _name = "pms.partner.search.param" _inherit = "pms.rest.metadata" id = fields.Integer(required=False, allow_none=True) + vatNumber = fields.String(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) diff --git a/pms_api_rest/datamodels/pms_payment.py b/pms_api_rest/datamodels/pms_payment.py deleted file mode 100644 index ade3dcbcc7..0000000000 --- a/pms_api_rest/datamodels/pms_payment.py +++ /dev/null @@ -1,12 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsPaymentInfo(Datamodel): - _name = "pms.payment.info" - id = fields.Integer(required=False, allow_none=True) - amount = fields.Float(required=False, allow_none=True) - journalId = fields.Integer(required=False, allow_none=True) - date = fields.String(required=False, allow_none=True) - memo = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_account_journal_service.py b/pms_api_rest/services/pms_account_journal_service.py index d39215bf64..69d211d299 100644 --- a/pms_api_rest/services/pms_account_journal_service.py +++ b/pms_api_rest/services/pms_account_journal_service.py @@ -23,29 +23,24 @@ class PmsAccountJournalService(Component): auth="jwt_api_pms", ) def get_method_payments(self, account_journal_search_param): - domain = [] - if account_journal_search_param.pmsPropertyId: - domain.extend( - [ - "|", - ( - "pms_property_ids", - "in", - account_journal_search_param.pmsPropertyId, - ), - ("pms_property_ids", "=", False), - ] - ) + pms_property = self.env["pms.property"].search( + [("id", "=", account_journal_search_param.pmsPropertyId)] + ) PmsAccountJournalInfo = self.env.datamodels["pms.account.journal.info"] result_account_journals = [] - for account_journal in self.env["account.journal"].search( - domain, - ): - result_account_journals.append( - PmsAccountJournalInfo( - id=account_journal.id, - name=account_journal.name, - allowedPayments=account_journal.allowed_pms_payments, + if not pms_property: + pass + else: + for method in pms_property._get_payment_methods(automatic_included=True): + payment_method = self.env["account.journal"].search( + [("id", "=", method.id)] ) - ) + result_account_journals.append( + PmsAccountJournalInfo( + id=payment_method.id, + name=payment_method.name, + allowedPayments=payment_method.allowed_pms_payments, + ) + ) + return result_account_journals diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 2771a19af6..842b3c0257 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -1,9 +1,10 @@ from datetime import datetime +from odoo.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 -from odoo.odoo.osv import expression class PmsPartnerService(Component): @@ -32,8 +33,11 @@ def get_partners(self, pms_partner_search_params): if pms_partner_search_params.name: domain_fields.append(("name", "ilike", pms_partner_search_params.name)) if pms_partner_search_params.housed: - partners_housed = self.env["pms.checkin.partner"].search([("state", "=", "onboard")]).mapped( - "partner_id") + partners_housed = ( + self.env["pms.checkin.partner"] + .search([("state", "=", "onboard")]) + .mapped("partner_id") + ) domain_fields.append(("id", "in", partners_housed.ids)) domain_filter = list() if pms_partner_search_params.filter: @@ -56,7 +60,7 @@ def get_partners(self, pms_partner_search_params): domain, order=pms_partner_search_params.orderBy, limit=pms_partner_search_params.limit, - offset=pms_partner_search_params.offset + offset=pms_partner_search_params.offset, ): checkouts = ( self.env["pms.checkin.partner"] @@ -141,16 +145,11 @@ def get_partners(self, pms_partner_search_params): invoiceToAgency=partner.invoice_to_agency if partner.invoice_to_agency else None, - agencyStateId=partner.state_id.id if partner.state_id else None, - agencyCity=partner.city if partner.city 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 - ) + return PmsPartnerResults(partners=result_partners, total=total_partners) @restapi.method( [ @@ -210,7 +209,7 @@ def get_partner_payments(self, partner_id): amount=round(payment.amount, 2), journalId=payment.journal_id.id, date=payment.date.strftime("%d/%m/%Y"), - memo=payment.ref, + reference=payment.ref, ) ) return payments @@ -384,7 +383,7 @@ def get_partner(self, pms_partner_search_params): ("aeat_identification", "=", pms_partner_search_params.vatNumber), ] partner = self.env["res.partner"].search(domain) - if not partner: + if not partner or len(partner) > 1: return PmsPartnerInfo() else: return PmsPartnerInfo( From 777dba97911e21e29d3212ce973ff5a251e7807e Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 6 Oct 2022 10:40:21 +0200 Subject: [PATCH 183/547] [IMP] pms-api-rest: booking engine folio service & room closure reasons --- pms_api_rest/datamodels/__init__.py | 2 + pms_api_rest/datamodels/pms_folio.py | 10 +- pms_api_rest/datamodels/pms_reservation.py | 2 + .../datamodels/pms_room_closure_reason.py | 12 +++ pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/pms_folio_service.py | 93 +++++++++++++------ .../pms_room_closure_reason_service.py | 42 +++++++++ pms_api_rest/services/pms_room_service.py | 30 ++++-- 8 files changed, 155 insertions(+), 37 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_room_closure_reason.py create mode 100644 pms_api_rest/services/pms_room_closure_reason_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 4d649c1704..52b777ba01 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -49,3 +49,5 @@ from . import res_users from . import res_lang from . import pms_account_payment_term + +from . import pms_room_closure_reason diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 8d20019adb..1e887cf1d8 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -1,11 +1,11 @@ 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" - pmsPropertyId = fields.Integer(required=True, allow_none=True) dateFrom = fields.String(required=False, allow_none=True) dateTo = fields.String(required=False, allow_none=True) @@ -24,6 +24,14 @@ class PmsFolioInfo(Datamodel): reservationType = fields.String(required=False, allow_none=True) pendingAmount = fields.Float(required=False, allow_none=True) lastCheckout = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=False) + partnerId = fields.Integer(required=False, allow_none=False) + reservations = fields.List(NestedModel("pms.reservation.info"), required=False, allow_none=False) + pricelistId = fields.Integer(required=False, allow_none=False) + saleChannelId = fields.Integer(required=False, allow_none=False) + agency = fields.Integer(required=False, allow_none=False) + externalReference = fields.String(required=False, allow_none=True) + closureReasonId = fields.Integer(required=False, allow_none=True) class PmsFolioShortInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 2bf69abe3a..1ffeadf20f 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -67,6 +67,8 @@ class PmsReservationInfo(Datamodel): priceOnlyRoom = fields.Float(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) + # TODO: Refact # messages = fields.List(fields.Dict(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..a93355d648 --- /dev/null +++ b/pms_api_rest/datamodels/pms_room_closure_reason.py @@ -0,0 +1,12 @@ +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/services/__init__.py b/pms_api_rest/services/__init__.py index 6017095d1c..2aa9295969 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -32,6 +32,7 @@ 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_account_payment_terms_service diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d50b008694..fd16c4bf9b 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -327,29 +327,70 @@ def get_folio_reservations(self, folio_id): return reservations - # @restapi.method( - # [ - # ( - # [ - # "/", - # ], - # "POST", - # ) - # ], - # input_param=Datamodel("pms.reservation.info", is_list=False), - # auth="jwt_api_pms", - # ) - # def create_reservation(self, pms_reservation_info): - # reservation = self.env["pms.reservation"].create( - # { - # "partner_name": pms_reservation_info.partner, - # "pms_property_id": pms_reservation_info.pmsPropertyId, - # "room_type_id": pms_reservation_info.roomTypeId, - # "pricelist_id": pms_reservation_info.pricelistId, - # "checkin": pms_reservation_info.checkin, - # "checkout": pms_reservation_info.checkout, - # "board_service_room_id": pms_reservation_info.boardServiceId, - # "channel_type_id": pms_reservation_info.channelTypeId, - # } - # ) - # return reservation.id + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.folio.info", is_list=False), + auth="jwt_api_pms", + ) + def create_folio(self, pms_folio_info): + 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, + } + else: + vals = { + "pms_property_id": pms_folio_info.pmsPropertyId, + "partner_id": pms_folio_info.partnerId, + "channel_type_id": pms_folio_info.saleChannelId, + "agency_id": pms_folio_info.agency, + "reservation_type": pms_folio_info.reservationType, + } + folio = self.env["pms.folio"].create(vals) + 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) + + return folio.id 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..89ccd1a7bc --- /dev/null +++ b/pms_api_rest/services/pms_room_closure_reason_service.py @@ -0,0 +1,42 @@ +from datetime import datetime, timedelta + +from odoo import _, fields +from odoo.exceptions import MissingError +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 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 + + ) + ) + return closure_reasons diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 4237541f54..db40ff67ea 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -37,7 +37,6 @@ def get_rooms(self, room_search_param): room_search_param.availabilityFrom and room_search_param.availabilityTo and room_search_param.pmsPropertyId - and room_search_param.pricelistId ): date_from = datetime.strptime( room_search_param.availabilityFrom, "%Y-%m-%d" @@ -48,16 +47,27 @@ def get_rooms(self, room_search_param): 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, - pricelist_id=room_search_param.pricelistId, - real_avail=True, - ) + 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 ( From eed6889577877084f09b6493cfcd8adb60b9679f Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 6 Oct 2022 10:48:55 +0200 Subject: [PATCH 184/547] [FIX] pms-api-rest: fix precommit --- pms_api_rest/datamodels/pms_folio.py | 4 +++- pms_api_rest/datamodels/pms_reservation.py | 5 +++-- .../datamodels/pms_room_closure_reason.py | 4 +--- pms_api_rest/services/pms_folio_service.py | 6 +++--- .../services/pms_room_closure_reason_service.py | 15 +++------------ 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 1e887cf1d8..ba427bf5e7 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -26,7 +26,9 @@ class PmsFolioInfo(Datamodel): lastCheckout = fields.String(required=False, allow_none=True) pmsPropertyId = fields.Integer(required=False, allow_none=False) partnerId = fields.Integer(required=False, allow_none=False) - reservations = fields.List(NestedModel("pms.reservation.info"), required=False, allow_none=False) + reservations = fields.List( + NestedModel("pms.reservation.info"), required=False, allow_none=False + ) pricelistId = fields.Integer(required=False, allow_none=False) saleChannelId = fields.Integer(required=False, allow_none=False) agency = fields.Integer(required=False, allow_none=False) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 1ffeadf20f..b439692282 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -67,8 +67,9 @@ class PmsReservationInfo(Datamodel): priceOnlyRoom = fields.Float(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) - + services = fields.List( + NestedModel("pms.service.info"), required=False, allow_none=True + ) # TODO: Refact # messages = fields.List(fields.Dict(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 index a93355d648..410d8b3e04 100644 --- a/pms_api_rest/datamodels/pms_room_closure_reason.py +++ b/pms_api_rest/datamodels/pms_room_closure_reason.py @@ -2,11 +2,9 @@ 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/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index fd16c4bf9b..dddefd89b8 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -340,7 +340,7 @@ def get_folio_reservations(self, folio_id): auth="jwt_api_pms", ) def create_folio(self, pms_folio_info): - if pms_folio_info.reservationType == 'out': + if pms_folio_info.reservationType == "out": vals = { "pms_property_id": pms_folio_info.pmsPropertyId, "reservation_type": pms_folio_info.reservationType, @@ -370,7 +370,7 @@ def create_folio(self, pms_folio_info): "reservation_type": pms_folio_info.reservationType, "children": reservation.children, } - reservation_record = self.env['pms.reservation'].create(vals) + reservation_record = self.env["pms.reservation"].create(vals) if reservation.services: for service in reservation.services: vals = { @@ -389,7 +389,7 @@ def create_folio(self, pms_folio_info): }, ) for line in service.serviceLines - ] + ], } self.env["pms.service"].create(vals) diff --git a/pms_api_rest/services/pms_room_closure_reason_service.py b/pms_api_rest/services/pms_room_closure_reason_service.py index 89ccd1a7bc..bbe3aa10c3 100644 --- a/pms_api_rest/services/pms_room_closure_reason_service.py +++ b/pms_api_rest/services/pms_room_closure_reason_service.py @@ -1,9 +1,3 @@ -from datetime import datetime, timedelta - -from odoo import _, fields -from odoo.exceptions import MissingError -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 @@ -30,13 +24,10 @@ class PmsClosureReasonService(Component): 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([]): + for cl in self.env["room.closure.reason"].search([]): closure_reasons.append( PmsRoomClosureReasonInfo( - id=cl.id, - name=cl.name, - description=cl.description - - ) + id=cl.id, name=cl.name, description=cl.description + ) ) return closure_reasons From 1c7e909ff0dde75a142a3c5a9727104586fe0a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 6 Oct 2022 12:38:14 +0200 Subject: [PATCH 185/547] [RFC]pms_api_rest: channel_type_id -> sale_channel_origin_id --- pms_api_rest/services/pms_folio_service.py | 6 +++--- pms_api_rest/services/pms_pricelist_service.py | 4 ++-- pms_api_rest/services/pms_reservation_service.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index dddefd89b8..ad31111c7d 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -136,8 +136,8 @@ def get_folios(self, folio_search_param): "pricelistId": reservation.pricelist_id.id if reservation.pricelist_id else None, - "saleChannelId": reservation.channel_type_id.id - if reservation.channel_type_id + "saleChannelId": reservation.sale_channel_origin_id.id + if reservation.sale_channel_origin_id else None, "agencyId": reservation.agency_id.id if reservation.agency_id @@ -350,7 +350,7 @@ def create_folio(self, pms_folio_info): vals = { "pms_property_id": pms_folio_info.pmsPropertyId, "partner_id": pms_folio_info.partnerId, - "channel_type_id": pms_folio_info.saleChannelId, + "sale_channel_origin_id": pms_folio_info.saleChannelId, "agency_id": pms_folio_info.agency, "reservation_type": pms_folio_info.reservationType, } diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 3a9a3c01db..4ef9b5cc1d 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -138,8 +138,8 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): ), ) - pricelist_info.pricelistItemId = item.id - pricelist_info.price = item.fixed_price + pricelist_info.pricelistItemId = item[0].id + pricelist_info.price = item[0].fixed_price result.append(pricelist_info) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 0fa9ada714..081c7bbd0e 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -64,8 +64,8 @@ def get_reservation(self, reservation_id, pms_search_param): boardServiceId=reservation.board_service_room_id.id if reservation.board_service_room_id else None, - saleChannelId=reservation.channel_type_id.id - if reservation.channel_type_id + 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, From 30c6ed826499522f8a6f60992118232ad8f7d360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 6 Oct 2022 12:53:29 +0200 Subject: [PATCH 186/547] [FIX]pms_api_rest: service price - return price --- pms_api_rest/services/pms_price_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_price_service.py b/pms_api_rest/services/pms_price_service.py index 1ede7859b6..a7a121896e 100644 --- a/pms_api_rest/services/pms_price_service.py +++ b/pms_api_rest/services/pms_price_service.py @@ -154,4 +154,4 @@ def _get_board_service_price( product_qty=product_qty, date_consumption=date_consumption, ) - return + return price From e8bbbc274592d4e105e8b25973f4f3d26cf590e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 6 Oct 2022 20:31:27 +0200 Subject: [PATCH 187/547] [FIX]pms_api_rest: import odoo.osv expression --- pms_api_rest/services/pms_partner_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 842b3c0257..394a8522af 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -1,6 +1,6 @@ from datetime import datetime -from odoo.odoo.osv import expression +from odoo.osv import expression from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel From e4476a2e29fb2d65d673b29386e8ebc209324810 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 10 Oct 2022 16:38:42 +0200 Subject: [PATCH 188/547] [FIX] pms-api-rest: fix av. plans by property --- pms_api_rest/services/pms_availability_plan_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 425363f265..e68b68f54f 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -120,6 +120,7 @@ def get_availability_plan_rules( record_availability_plan_id.id, ), ("room_type_id", "=", room_type.id), + ("pms_property_id", "=", availability_plan_rule_search_param.pmsPropertyId) ] ) if rule: From d73fb2a9cc1d4c777b3410318961897203d9053b Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 10 Oct 2022 17:04:43 +0200 Subject: [PATCH 189/547] [WIP] pms-api-rest: change route patch reservation (CORS issue) --- pms_api_rest/services/pms_reservation_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 081c7bbd0e..7aa7f37646 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -120,7 +120,7 @@ def get_reservation(self, reservation_id, pms_search_param): [ ( [ - "/", + "/p/", ], "PATCH", ) @@ -128,6 +128,7 @@ def get_reservation(self, reservation_id, pms_search_param): 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 = {} From cd2ee1a0b3ded9d36e52b811b8529fa2a89be017 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 10 Oct 2022 17:31:58 +0200 Subject: [PATCH 190/547] [IMP] pms-api-rest: assign reservation room --- pms_api_rest/services/pms_reservation_service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 7aa7f37646..d831987cea 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -188,6 +188,9 @@ def update_reservation(self, reservation_id, reservation_data): reservation_vals.update( {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} ) + if reservation_data.toAssign is not None and not reservation_data.toAssign: + reservation.action_assign() + reservation.write(reservation_vals) def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): From a1b9004e39c5f066b57a21ebc5a058f1ea7e6d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 11 Oct 2022 09:47:50 +0200 Subject: [PATCH 191/547] [FIX]:pms_api_rest: user without userImage -> userImageBase64 to none --- pms_api_rest/services/pms_login_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 95f650437d..75281b8750 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -71,6 +71,6 @@ def login(self, user): userName=user_record.name, defaultPropertyId=user_record.pms_property_id.id, defaultPropertyName=user_record.pms_property_id.name, - userImageBase64=user_record.partner_id.image_1024, + userImageBase64=user_record.partner_id.image_1024 or None, availabilityRuleFields=avail_rule_names, ) From d81e61c2877683f7f9351d024d0c854148e7b503 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 11 Oct 2022 12:34:52 +0200 Subject: [PATCH 192/547] [FIX] pms-api-rest: fix filter by property @ cancelation rule service --- pms_api_rest/services/pms_cancelation_rule_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_cancelation_rule_service.py b/pms_api_rest/services/pms_cancelation_rule_service.py index 35c15bbf3f..8bf2cb60a7 100644 --- a/pms_api_rest/services/pms_cancelation_rule_service.py +++ b/pms_api_rest/services/pms_cancelation_rule_service.py @@ -34,7 +34,7 @@ def get_cancelation_rules(self, cancelation_rule_search_param): ) if cancelation_rule_search_param.pmsPropertyId: domain.append( - ("pms_property_ids", "in", cancelation_rule_search_param.pmsPropertyId) + ("pms_property_ids", "in", [cancelation_rule_search_param.pmsPropertyId]) ) result_cancelation_rules = [] From f2df84188d93d1da797e58c4f31151754c607273 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 11 Oct 2022 12:42:47 +0200 Subject: [PATCH 193/547] [REF]pms-api-rest: partner and partners GET --- .../services/pms_availability_plan_service.py | 6 ++++- .../services/pms_cancelation_rule_service.py | 6 ++++- pms_api_rest/services/pms_partner_service.py | 26 +++++++++---------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index e68b68f54f..18651bfa1f 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -120,7 +120,11 @@ def get_availability_plan_rules( record_availability_plan_id.id, ), ("room_type_id", "=", room_type.id), - ("pms_property_id", "=", availability_plan_rule_search_param.pmsPropertyId) + ( + "pms_property_id", + "=", + availability_plan_rule_search_param.pmsPropertyId, + ), ] ) if rule: diff --git a/pms_api_rest/services/pms_cancelation_rule_service.py b/pms_api_rest/services/pms_cancelation_rule_service.py index 8bf2cb60a7..e169e634ab 100644 --- a/pms_api_rest/services/pms_cancelation_rule_service.py +++ b/pms_api_rest/services/pms_cancelation_rule_service.py @@ -34,7 +34,11 @@ def get_cancelation_rules(self, cancelation_rule_search_param): ) if cancelation_rule_search_param.pmsPropertyId: domain.append( - ("pms_property_ids", "in", [cancelation_rule_search_param.pmsPropertyId]) + ( + "pms_property_ids", + "in", + [cancelation_rule_search_param.pmsPropertyId], + ) ) result_cancelation_rules = [] diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 394a8522af..83c51a1cf8 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -48,7 +48,12 @@ def get_partners(self, pms_partner_search_params): [("lastname", "ilike", search)], ] domain_filter.append(expression.OR(subdomains)) - domain = [] + if pms_partner_search_params.vatNumber: + domain_fields = [ + "|", + ("vat", "ilike", pms_partner_search_params.vatNumber), + ("aeat_identification", "ilike", pms_partner_search_params.vatNumber), + ] if domain_filter: domain = expression.AND([domain_fields, domain_filter[0]]) else: @@ -165,7 +170,8 @@ def get_partners(self, pms_partner_search_params): ) def create_partner(self, partner_info): vals = self.mapping_partner_values(partner_info) - self.env["res.partner"].create(vals) + partner = self.env["res.partner"].create(vals) + return partner.id @restapi.method( [ @@ -364,26 +370,18 @@ def activate_partner(self, partner_id): [ ( [ - "/partner", + "/", ], "GET", ) ], - input_param=Datamodel("pms.partner.search.param", is_list=False), output_param=Datamodel("pms.partner.info", is_list=False), auth="jwt_api_pms", ) - def get_partner(self, pms_partner_search_params): - domain = [] + def get_partner(self, partner_id): PmsPartnerInfo = self.env.datamodels["pms.partner.info"] - if pms_partner_search_params.vatNumber: - domain = [ - "|", - ("vat", "=", pms_partner_search_params.vatNumber), - ("aeat_identification", "=", pms_partner_search_params.vatNumber), - ] - partner = self.env["res.partner"].search(domain) - if not partner or len(partner) > 1: + partner = self.env["res.partner"].browse(partner_id) + if not partner: return PmsPartnerInfo() else: return PmsPartnerInfo( From 990d54da0a0e49c5bc838d8079b753a48baa7902 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 11 Oct 2022 19:59:47 +0200 Subject: [PATCH 194/547] [FIX]pms_api_rest: added lastname2 to mapping_checkin_partner_values and ref get_partner_by_doc_numer --- pms_api_rest/services/pms_partner_service.py | 16 +++++++++------- pms_api_rest/services/pms_reservation_service.py | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 83c51a1cf8..f9c7b39a1e 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -269,11 +269,14 @@ def get_partner_invoices(self, partner_id): auth="jwt_api_pms", ) def get_partner_by_doc_number(self, document_type, document_number): + doc_type = self.env["res.partner.id_category"].search( + [("id", "=", document_type)] + ) doc_number = self.env["res.partner.id_number"].search( - [("name", "=", document_number), ("category_id", "=", int(document_type))] + [("name", "=", document_number), ("category_id", "=", doc_type.id)] ) partners = [] - PmsPartnerInfo = self.env.datamodels["pms.partner.info"] + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] if not doc_number: pass else: @@ -284,8 +287,8 @@ def get_partner_by_doc_number(self, document_type, document_number): "%d/%m/%Y" ) partners.append( - PmsPartnerInfo( - # id=doc_number.partner_id.id, + PmsCheckinPartnerInfo( + id=doc_number.partner_id.id, name=doc_number.partner_id.name if doc_number.partner_id.name else None, @@ -304,7 +307,7 @@ def get_partner_by_doc_number(self, document_type, document_number): mobile=doc_number.partner_id.mobile if doc_number.partner_id.mobile else None, - documentType=int(document_type), + documentType=doc_type.id, documentNumber=doc_number.name, documentExpeditionDate=document_expedition_date if doc_number.valid_from @@ -321,7 +324,7 @@ def get_partner_by_doc_number(self, document_type, document_number): residenceStreet=doc_number.partner_id.residence_street if doc_number.partner_id.residence_street else None, - residenceZip=doc_number.partner_id.residence_zip + zip=doc_number.partner_id.residence_zip if doc_number.partner_id.residence_zip else None, residenceCity=doc_number.partner_id.residence_city @@ -330,7 +333,6 @@ def get_partner_by_doc_number(self, document_type, document_number): nationality=doc_number.partner_id.nationality_id.id if doc_number.partner_id.nationality_id else None, - # aquí tiene que ser id countryState=doc_number.partner_id.residence_state_id.id if doc_number.partner_id.residence_state_id else None, diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index d831987cea..8c38e3d464 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -661,6 +661,7 @@ def mapping_checkin_partner_values(self, pms_checkin_partner_info): checkin_partner_fields = { "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, From 4755f0ee05da00010a3503bdc4b06dbe78245742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 12 Oct 2022 11:09:47 +0200 Subject: [PATCH 195/547] [TMP]14.0-pms_api_rest: Temporal fix to patch routes avoid CORS --- pms_api_rest/services/pms_calendar_service.py | 2 +- pms_api_rest/services/pms_partner_service.py | 6 +++--- pms_api_rest/services/pms_reservation_line_service.py | 2 +- pms_api_rest/services/pms_reservation_service.py | 4 ++-- pms_api_rest/services/pms_room_service.py | 2 +- pms_api_rest/services/pms_service_line_service.py | 2 +- pms_api_rest/services/pms_service_service.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 8932cd32b8..9aa3296d1e 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -299,7 +299,7 @@ def get_alerts_per_day(self, pms_calendar_search_param): [ ( [ - "/", + "/p/", ], "PATCH", ) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index f9c7b39a1e..bcb1806322 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -177,7 +177,7 @@ def create_partner(self, partner_info): [ ( [ - "/", + "/p/", ], "PATCH", ) @@ -344,7 +344,7 @@ def get_partner_by_doc_number(self, document_type, document_number): [ ( [ - "//deactivate", + "/p//deactivate", ], "PATCH", ) @@ -358,7 +358,7 @@ def deactivate_partner(self, partner_id): [ ( [ - "//activate", + "/p//activate", ], "PATCH", ) diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index 47308e62c4..fee38b3030 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -104,7 +104,7 @@ def get_reservation_line(self, reservation_line_id): [ ( [ - "/", + "/p/", ], "PATCH", ) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 8c38e3d464..bd688fd94c 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -322,7 +322,7 @@ def delete_reservation_line(self, reservation_id, reservation_line_id): [ ( [ - "//reservation-lines/", + "/p//reservation-lines/", ], "PATCH", ) @@ -521,7 +521,7 @@ def get_checkin_partners(self, reservation_id): [ ( [ - "//checkin-partners/", + "/p//checkin-partners/", ], "PATCH", ) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index db40ff67ea..92fe8892d2 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -126,7 +126,7 @@ def get_room(self, room_id): [ ( [ - "/", + "/p/", ], "PATCH", ) diff --git a/pms_api_rest/services/pms_service_line_service.py b/pms_api_rest/services/pms_service_line_service.py index a24f225337..b34de7baf0 100644 --- a/pms_api_rest/services/pms_service_line_service.py +++ b/pms_api_rest/services/pms_service_line_service.py @@ -46,7 +46,7 @@ def get_service_line(self, service_line_id): [ ( [ - "/", + "/p/", ], "PATCH", ) diff --git a/pms_api_rest/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py index 0e2aa42846..5a48913ad6 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -61,7 +61,7 @@ def get_service(self, service_id): [ ( [ - "/", + "/p/", ], "PATCH", ) From 2bf9868bbff664ae6dd92e844d10cb57531343a0 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 12 Oct 2022 18:49:25 +0200 Subject: [PATCH 196/547] [FIX] pms-api-rest: partner service error when no checkouts --- pms_api_rest/services/pms_partner_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index bcb1806322..bdcbbc08a2 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -70,6 +70,7 @@ def get_partners(self, pms_partner_search_params): checkouts = ( self.env["pms.checkin.partner"] .search([("partner_id.id", "=", partner.id)]) + .filtered(lambda x: x.checkout) .mapped("checkout") ) result_partners.append( From 5ea69f9ff13826fb18ea98c4ff62533de2d8428e Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 13 Oct 2022 17:06:11 +0200 Subject: [PATCH 197/547] [FIX]pms_api_rest: added residence_country_id in checkin_partner datamodel and services --- .../datamodels/pms_checkin_partner.py | 1 + pms_api_rest/datamodels/res_city_zip.py | 4 ++-- pms_api_rest/services/pms_partner_service.py | 3 +++ .../services/pms_reservation_service.py | 5 ++++- pms_api_rest/services/res_city_zip_service.py | 21 ++++++++++--------- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py index 045cc27e23..2f5fdd7b48 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner.py +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -24,4 +24,5 @@ class PmsCheckinPartnerInfo(Datamodel): residenceCity = fields.String(required=False, allow_none=True) nationality = fields.Integer(required=False, allow_none=True) countryState = fields.Integer(required=False, allow_none=True) + countryId = fields.Integer(required=False, allow_none=True) checkinPartnerState = 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 index aca9f2d123..36a54f2cad 100644 --- a/pms_api_rest/datamodels/res_city_zip.py +++ b/pms_api_rest/datamodels/res_city_zip.py @@ -6,5 +6,5 @@ class ResCityZipInfo(Datamodel): _name = "res.city.zip.info" cityId = fields.String(required=False, allow_none=True) - stateId = fields.String(required=False, allow_none=True) - countryId = fields.String(required=False, allow_none=True) + stateId = fields.Integer(required=False, allow_none=True) + countryId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index bdcbbc08a2..444a51869a 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -334,6 +334,9 @@ def get_partner_by_doc_number(self, document_type, document_number): nationality=doc_number.partner_id.nationality_id.id if doc_number.partner_id.nationality_id else None, + countryId=doc_number.partner_id.residence_country_id + if doc_number.partner_id.residence_country_id + else None, countryState=doc_number.partner_id.residence_state_id.id if doc_number.partner_id.residence_state_id else None, diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index bd688fd94c..b2eabadc2e 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -512,6 +512,9 @@ def get_checkin_partners(self, reservation_id): countryState=checkin_partner.residence_state_id.id 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, ) ) @@ -673,7 +676,7 @@ def mapping_checkin_partner_values(self, pms_checkin_partner_info): "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.nationality, + "residence_country_id": pms_checkin_partner_info.countryId, } if pms_checkin_partner_info.documentExpeditionDate: document_expedition_date = datetime.strptime( diff --git a/pms_api_rest/services/res_city_zip_service.py b/pms_api_rest/services/res_city_zip_service.py index fa35ab4944..92ad714983 100644 --- a/pms_api_rest/services/res_city_zip_service.py +++ b/pms_api_rest/services/res_city_zip_service.py @@ -18,18 +18,19 @@ class ResCityZipService(Component): "GET", ) ], - output_param=Datamodel("res.city.zip.info", is_list=True), + output_param=Datamodel("res.city.zip.info", is_list=False), auth="jwt_api_pms", ) def get_address_data_by_zip(self, res_city_zip): - result_zip_data = [] ResCityZipInfo = self.env.datamodels["res.city.zip.info"] - for zip_code in self.env["res.city.zip"].search([("name", "=", res_city_zip)]): - result_zip_data.append( - ResCityZipInfo( - cityId=zip_code.city_id.name, - stateId=zip_code.state_id.name, - countryId=zip_code.country_id.name, - ) + res_zip = self.env["res.city.zip"].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, ) - return result_zip_data + else: + return ResCityZipInfo() From 54f48d4a374d8efc600b57402f7b25f8a6c495fc Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Oct 2022 16:14:30 +0200 Subject: [PATCH 198/547] [FIX] pms-api-rest: fix filter by property (filter param in pms_property_ids or pms_property_ids = false ) @ cancelation rules service --- .../services/pms_cancelation_rule_service.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pms_api_rest/services/pms_cancelation_rule_service.py b/pms_api_rest/services/pms_cancelation_rule_service.py index e169e634ab..2f16b5cbdf 100644 --- a/pms_api_rest/services/pms_cancelation_rule_service.py +++ b/pms_api_rest/services/pms_cancelation_rule_service.py @@ -30,17 +30,20 @@ def get_cancelation_rules(self, cancelation_rule_search_param): if cancelation_rule_search_param.pricelistId: domain.append( - ("pricelist_ids", "in", cancelation_rule_search_param.pricelistId) + ("pricelist_ids", "in", [cancelation_rule_search_param.pricelistId]) ) if cancelation_rule_search_param.pmsPropertyId: - domain.append( - ( - "pms_property_ids", - "in", - [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( From 0fc3f099525c78b32186c95e4664f04cc6622bd0 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Oct 2022 19:20:08 +0200 Subject: [PATCH 199/547] [IMP] pms-pwa: add avail. plan id to response @ av. plan rules service --- .../services/pms_availability_plan_service.py | 1 + .../services/pms_cancelation_rule_service.py | 20 +++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 18651bfa1f..98f117d16e 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -141,6 +141,7 @@ def get_availability_plan_rules( closedArrival=rule.closed_arrival, quota=rule.quota if rule.quota != -1 else 0, maxAvailability=rule.max_avail, + availabilityPlanId=rule.availability_plan_id, ) result.append(availability_plan_rule_info) diff --git a/pms_api_rest/services/pms_cancelation_rule_service.py b/pms_api_rest/services/pms_cancelation_rule_service.py index 2f16b5cbdf..d0616f0325 100644 --- a/pms_api_rest/services/pms_cancelation_rule_service.py +++ b/pms_api_rest/services/pms_cancelation_rule_service.py @@ -34,16 +34,16 @@ def get_cancelation_rules(self, cancelation_rule_search_param): ) if cancelation_rule_search_param.pmsPropertyId: domain.extend( - [ - '|', - ( - "pms_property_ids", - "in", - [cancelation_rule_search_param.pmsPropertyId], - ), - ("pms_property_ids", "=", False), - ] - ) + [ + "|", + ( + "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( From 7f7960af4d26c2cc2815710f72df576eca4db7f4 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 14 Oct 2022 10:36:25 +0200 Subject: [PATCH 200/547] [IMP]pms_api_rest: added action_cancel and confirm in reservation patch service --- pms_api_rest/services/pms_reservation_service.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index b2eabadc2e..03d8c85c9d 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -190,8 +190,12 @@ def update_reservation(self, reservation_id, reservation_data): ) if reservation_data.toAssign is not None and not reservation_data.toAssign: reservation.action_assign() - - reservation.write(reservation_vals) + if reservation_data.stateCode == "cancel": + reservation.action_cancel() + if reservation_data.stateCode == "confirm": + reservation.confirm() + if reservation_vals: + reservation.write(reservation_vals) def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): # Return dict witch reservation.lines values (only modified if line exist, From 5e7c977824dfc619f0f69764a322f0657800bd42 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 17 Oct 2022 16:46:45 +0200 Subject: [PATCH 201/547] [FIX] ps-api-rest: filter parterns by name --- pms_api_rest/services/pms_partner_service.py | 24 ++++++-------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 444a51869a..8166873408 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -28,39 +28,29 @@ class PmsPartnerService(Component): ) def get_partners(self, pms_partner_search_params): result_partners = [] - domain_fields = [] + domain = [] if pms_partner_search_params.name: - domain_fields.append(("name", "ilike", pms_partner_search_params.name)) + domain.append(("name", "ilike", pms_partner_search_params.name)) if pms_partner_search_params.housed: partners_housed = ( self.env["pms.checkin.partner"] .search([("state", "=", "onboard")]) .mapped("partner_id") ) - domain_fields.append(("id", "in", partners_housed.ids)) - domain_filter = list() + domain.append(("id", "in", partners_housed.ids)) if pms_partner_search_params.filter: - for search in pms_partner_search_params.filter.split(" "): - subdomains = [ - [("name", "ilike", search)], - [("firstname", "ilike", search)], - [("lastname", "ilike", search)], - ] - domain_filter.append(expression.OR(subdomains)) + domain.append(("display_name", "ilike", pms_partner_search_params.filter)) if pms_partner_search_params.vatNumber: - domain_fields = [ + domain.append([ "|", ("vat", "ilike", pms_partner_search_params.vatNumber), ("aeat_identification", "ilike", pms_partner_search_params.vatNumber), - ] - if domain_filter: - domain = expression.AND([domain_fields, domain_filter[0]]) - else: - domain = domain_fields + ]) 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, order=pms_partner_search_params.orderBy, From 6e5b760673ca2eebbeb280081c88cf375b900aa6 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 17 Oct 2022 18:29:15 +0200 Subject: [PATCH 202/547] [IMP] pms-api-rest: add field to partner input datamodel --- pms_api_rest/datamodels/pms_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index ae8078c214..02f3a6dbea 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -8,7 +8,7 @@ class PmsPartnerSearchParam(Datamodel): _name = "pms.partner.search.param" _inherit = "pms.rest.metadata" id = fields.Integer(required=False, allow_none=True) - vatNumber = fields.String(required=False, allow_none=True) + vatNumberOrName = fields.String(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) From 5491d44ae7051d3a7eb5af755cf252e7a8cdd0b3 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 17 Oct 2022 18:29:35 +0200 Subject: [PATCH 203/547] [FIX] pms-pwa: fix precommit --- pms_api_rest/services/pms_partner_service.py | 21 ++++++--- .../services/pms_reservation_service.py | 44 +++++++++++-------- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 8166873408..b5e4c0c607 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -41,12 +41,21 @@ def get_partners(self, pms_partner_search_params): domain.append(("id", "in", partners_housed.ids)) if pms_partner_search_params.filter: domain.append(("display_name", "ilike", pms_partner_search_params.filter)) - if pms_partner_search_params.vatNumber: - domain.append([ - "|", - ("vat", "ilike", pms_partner_search_params.vatNumber), - ("aeat_identification", "ilike", pms_partner_search_params.vatNumber), - ]) + if pms_partner_search_params.vatNumberOrName: + subdomains = [ + [("vat", "ilike", pms_partner_search_params.vatNumberOrName)], + [ + ( + "aeat_identification", + "ilike", + pms_partner_search_params.vatNumberOrName, + ) + ], + [("display_name", "ilike", pms_partner_search_params.vatNumberOrName)], + ] + domain_vat_or_name = expression.OR(subdomains) + domain = expression.AND([domain, domain_vat_or_name]) + PmsPartnerResults = self.env.datamodels["pms.partner.results"] PmsPartnerInfo = self.env.datamodels["pms.partner.info"] total_partners = self.env["res.partner"].search_count(domain) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 03d8c85c9d..2334dd09d2 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -128,6 +128,28 @@ def get_reservation(self, reservation_id, pms_search_param): input_param=Datamodel("pms.reservation.info", is_list=False), auth="jwt_api_pms", ) + def _create_vals_from_params(self, reservation_vals, reservation_data): + if reservation_data.preferredRoomId: + reservation_vals.update( + {"preferred_room_id": reservation_data.preferredRoomId} + ) + if reservation_data.boardServiceId: + reservation_vals.update( + {"board_service_room_id": reservation_data.boardServiceId} + ) + 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: + reservation_vals.update({"children": reservation_data.children}) + if reservation_data.segmentationId: + reservation_vals.update( + {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} + ) + + return reservation_vals + # 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)]) @@ -170,30 +192,16 @@ def update_reservation(self, reservation_id, reservation_data): } ) - if reservation_data.preferredRoomId: - reservation_vals.update( - {"preferred_room_id": reservation_data.preferredRoomId} - ) - if reservation_data.boardServiceId: - reservation_vals.update( - {"board_service_room_id": reservation_data.boardServiceId} - ) - 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: - reservation_vals.update({"children": reservation_data.children}) - if reservation_data.segmentationId: - reservation_vals.update( - {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} - ) 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.confirm() + + reservation_vals = self._create_vals_from_params( + reservation_vals, reservation_data + ) if reservation_vals: reservation.write(reservation_vals) From a9ae064faca5929ad05adf3bb859a167306975e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 19 Oct 2022 17:16:46 +0200 Subject: [PATCH 204/547] [FIX]pms_api_rest: sintax error with _create_vals_from_params in reservation service --- .../services/pms_reservation_service.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 2334dd09d2..4e22785869 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -116,18 +116,6 @@ def get_reservation(self, reservation_id, pms_search_param): ) return res - @restapi.method( - [ - ( - [ - "/p/", - ], - "PATCH", - ) - ], - input_param=Datamodel("pms.reservation.info", is_list=False), - auth="jwt_api_pms", - ) def _create_vals_from_params(self, reservation_vals, reservation_data): if reservation_data.preferredRoomId: reservation_vals.update( @@ -148,8 +136,18 @@ def _create_vals_from_params(self, reservation_vals, reservation_data): {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} ) - 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)]) From 5cb384dbfc40369fce3afccc6b67f4f24d41e5d8 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 19 Oct 2022 14:53:05 +0200 Subject: [PATCH 205/547] [IMP]pms_api_rest: added action_on_board, action_checkout and print checkin pdf services --- pms_api_rest/datamodels/__init__.py | 1 + .../datamodels/pms_checkin_partner.py | 1 + pms_api_rest/datamodels/pms_report.py | 7 ++ pms_api_rest/datamodels/pms_reservation.py | 1 + .../services/pms_reservation_service.py | 69 ++++++++++++++++++- 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 pms_api_rest/datamodels/pms_report.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 52b777ba01..4cb78ab875 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -51,3 +51,4 @@ from . import pms_account_payment_term from . import pms_room_closure_reason +from . import pms_report diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py index 2f5fdd7b48..cbab413297 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner.py +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -26,3 +26,4 @@ class PmsCheckinPartnerInfo(Datamodel): countryState = fields.Integer(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) diff --git a/pms_api_rest/datamodels/pms_report.py b/pms_api_rest/datamodels/pms_report.py new file mode 100644 index 0000000000..d49a885a74 --- /dev/null +++ b/pms_api_rest/datamodels/pms_report.py @@ -0,0 +1,7 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + +class PmsReportInfo(Datamodel): + _name = "pms.report.info" + pdf = 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 index b439692282..0c69fbb489 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -58,6 +58,7 @@ class PmsReservationInfo(Datamodel): 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) reservationType = fields.String(required=False, allow_none=True) priceTotal = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 4e22785869..a4f9dd2dd6 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -1,3 +1,4 @@ +import base64 from datetime import datetime, timedelta from odoo import _ @@ -196,6 +197,8 @@ def update_reservation(self, reservation_id, reservation_data): reservation.action_cancel() if reservation_data.stateCode == "confirm": reservation.confirm() + if reservation_data.toCheckout is not None and reservation_data.toCheckout: + reservation.action_reservation_checkout() reservation_vals = self._create_vals_from_params( reservation_vals, reservation_data @@ -548,6 +551,13 @@ def write_reservation_checkin_partner( checkin_partner = self.env["pms.checkin.partner"].search( [("id", "=", checkin_partner_id), ("reservation_id", "=", reservation_id)] ) + if ( + pms_checkin_partner_info.actionOnBoard + and pms_checkin_partner_info.actionOnBoard is not None + and pms_checkin_partner_info.actionOnBoard + and checkin_partner + ): + checkin_partner.action_on_board() if checkin_partner: checkin_partner.write( self.mapping_checkin_partner_values(pms_checkin_partner_info) @@ -682,7 +692,7 @@ def mapping_checkin_partner_values(self, pms_checkin_partner_info): "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, + "nationality_id": pms_checkin_partner_info.countryId, "residence_zip": pms_checkin_partner_info.zip, "residence_city": pms_checkin_partner_info.residenceCity, "residence_state_id": pms_checkin_partner_info.countryState, @@ -704,3 +714,60 @@ def mapping_checkin_partner_values(self, pms_checkin_partner_info): if v: vals.update({k: v}) return vals + + @restapi.method( + [ + ( + [ + "//checkin-report", + ], + "GET", + ) + ], + auth="jwt_api_pms", + output_param=Datamodel("pms.report.info", 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.info"] + return PmsResponse(pdf=base64EncodedStr) + + @restapi.method( + [ + ( + [ + "//checkin-partners/" + "/checkin-report", + ], + "GET", + ) + ], + auth="jwt_api_pms", + output_param=Datamodel("pms.report.info", 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.info"] + return PmsResponse(pdf=base64EncodedStr) From 57fb2e23604cf691c69290e2014637b7a3a8f36c Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 19 Oct 2022 17:54:55 +0200 Subject: [PATCH 206/547] [FIX] pms-api-rest: fix patch reservation update room type id --- pms_api_rest/services/pms_reservation_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index a4f9dd2dd6..ebfa0b0376 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -199,6 +199,8 @@ def update_reservation(self, reservation_id, reservation_data): reservation.confirm() if reservation_data.toCheckout is not None and reservation_data.toCheckout: reservation.action_reservation_checkout() + if reservation_data.roomTypeId: + reservation.room_type_id = reservation_data.roomTypeId reservation_vals = self._create_vals_from_params( reservation_vals, reservation_data From 9f52e38cfe41b27a20eceb42c2312c152909f93a Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 20 Oct 2022 10:33:59 +0200 Subject: [PATCH 207/547] [FIX] pms-api-rest: fix patch reservation update --- pms_api_rest/datamodels/pms_report.py | 1 + pms_api_rest/services/pms_reservation_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_report.py b/pms_api_rest/datamodels/pms_report.py index d49a885a74..3fea07f6e3 100644 --- a/pms_api_rest/datamodels/pms_report.py +++ b/pms_api_rest/datamodels/pms_report.py @@ -2,6 +2,7 @@ from odoo.addons.datamodel.core import Datamodel + class PmsReportInfo(Datamodel): _name = "pms.report.info" pdf = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index ebfa0b0376..ac0ea8a9ed 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -136,6 +136,7 @@ def _create_vals_from_params(self, reservation_vals, reservation_data): reservation_vals.update( {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} ) + return reservation_vals @restapi.method( [ From 8aa39ec6308de99648dc743726a3d401a090fa61 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 21 Oct 2022 13:26:11 +0200 Subject: [PATCH 208/547] [FIX]pms_api_rest: vat_number, vat_document_type and mobile fields in partner GET services --- pms_api_rest/services/pms_partner_service.py | 23 +++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index b5e4c0c607..1683dc6ed1 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -88,7 +88,7 @@ def get_partners(self, pms_partner_search_params): if partner.birthdate_date else None, age=partner.age if partner.age else None, - mobile=str(partner.mobile), + mobile=partner.mobile if partner.mobile else None, residenceStreet=partner.residence_street if partner.residence_street else None, @@ -118,9 +118,15 @@ def get_partners(self, pms_partner_search_params): residenceCountryId=partner.residence_country_id.id if partner.residence_country_id else None, - vatNumber=partner.vat if partner.vat else None, - vatDocumentType=partner.vat_document_type + vatNumber=partner.vat + if partner.vat + else partner.aeat_identification + if partner.aeat_identification + else None, + vatDocumentType="02" if partner.vat_document_type + 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, @@ -432,9 +438,15 @@ def get_partner(self, partner_id): residenceCountryId=partner.residence_country_id.id if partner.residence_country_id else None, - vatNumber=partner.vat if partner.vat else None, - vatDocumentType=partner.vat_document_type + vatNumber=partner.vat + if partner.vat + else partner.aeat_identification + if partner.aeat_identification + else None, + vatDocumentType="02" if partner.vat_document_type + 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, @@ -469,7 +481,6 @@ def get_partner(self, partner_id): def mapping_partner_values(self, pms_partner_info): vals = dict() partner_fields = { - "name": pms_partner_info.name, "firstname": pms_partner_info.firstname, "lastname": pms_partner_info.lastname, "lastname2": pms_partner_info.lastname2, From 4f2ebe46a0abd0739d1fcbc35815e6e10f690572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 23 Oct 2022 17:23:21 +0200 Subject: [PATCH 209/547] [IMP]pms_api_rest: improvement calendar load performance --- pms_api_rest/services/pms_calendar_service.py | 238 ++++++++++++------ 1 file changed, 164 insertions(+), 74 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 9aa3296d1e..486dc6a66e 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -25,64 +25,121 @@ class PmsCalendarService(Component): auth="jwt_api_pms", ) def get_calendar(self, calendar_search_param): - domain = list() - domain.append(("date", ">=", calendar_search_param.dateFrom)) - domain.append(("date", "<=", calendar_search_param.dateTo)) - domain.append(("pms_property_id", "=", calendar_search_param.pmsPropertyId)) - domain.append(("state", "!=", "cancel")) - result_lines = [] + 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 + self.env.cr.execute( + """ + SELECT night.id as id, night.state, DATE(night.date), night.room_id, + pms_room_type.default_code, reservation.to_assign, reservation.splitted, + reservation.partner_id, reservation.partner_name, folio.id, reservation.id, + reservation.name, reservation.reservation_type, reservation.checkin, + reservation.checkout, reservation.price_total, folio.pending_amount, + reservation.adults + 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') + """, + ( + pms_property_id, + tuple(target_dates), + ), + ) + SQL_FIELDS = [ + "id", + "state", + "date", + "room_id", + "room_type_name", + "to_assign", + "splitted", + "partner_id", + "partner_name", + "folio_id", + "reservation_id", + "reservation_name", + "reservation_type", + "checkin", + "checkout", + "price_total", + "folio_pending_amount", + "adults", + ] + result_sql = self.env.cr.fetchall() + lines = [] + for res in result_sql: + lines.append({field: res[SQL_FIELDS.index(field)] for field in SQL_FIELDS}) + PmsCalendarInfo = self.env.datamodels["pms.calendar.info"] - for line in self.env["pms.reservation.line"].search( - domain, - ): + result_lines = [] + for line in lines: next_line_splitted = False - next_line = self.env["pms.reservation.line"].search( - [ - ("reservation_id", "=", line.reservation_id.id), - ("date", "=", line.date + timedelta(days=1)), - ] - ) - if next_line: - next_line_splitted = next_line.room_id != line.room_id - previous_line_splitted = False - previous_line = self.env["pms.reservation.line"].search( - [ - ("reservation_id", "=", line.reservation_id.id), - ("date", "=", line.date + timedelta(days=-1)), - ] - ) - if previous_line: - previous_line_splitted = previous_line.room_id != line.room_id + 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.reservation_id.state, - date=datetime.combine(line.date, datetime.min.time()).isoformat(), - roomId=line.room_id.id, - roomTypeName=str(line.reservation_id.room_type_id.default_code), - toAssign=line.reservation_id.to_assign, - splitted=line.reservation_id.splitted, - partnerId=line.reservation_id.partner_id.id or None, - partnerName=line.reservation_id.partner_name or None, - folioId=line.reservation_id.folio_id, - reservationId=line.reservation_id, - reservationName=line.reservation_id.name, - reservationType=line.reservation_id.reservation_type, - isFirstNight=line.reservation_id.checkin == line.date, - isLastNight=line.reservation_id.checkout + timedelta(days=-1) - == line.date, - totalPrice=round(line.reservation_id.price_total, 2), - pendingPayment=round(line.reservation_id.folio_pending_amount, 2), - numNotifications=line.reservation_id.message_needaction_counter, - adults=line.reservation_id.adults, + 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), + # TODO: line.reservation_id.message_needaction_counter is computed field, + numNotifications=0, + adults=line["adults"], nextLineSplitted=next_line_splitted, previousLineSplitted=previous_line_splitted, - hasNextLine=bool(next_line), - closureReason=line.reservation_id.closure_reason_id.name - if line.reservation_id.closure_reason_id - else "", + hasNextLine=not is_last_night, # REVIEW: redundant with isLastNight? + closureReason=line[ + "partner_name" + ], # REVIEW: is necesary closure_reason_id? ) ) return result_lines @@ -144,42 +201,75 @@ def swap_reservation_slices(self, swap_info): auth="jwt_api_pms", ) def get_daily_invoincing(self, pms_calendar_search_param): - reservation_lines = self.env["pms.reservation.line"].search( - [ - ("date", ">=", pms_calendar_search_param.dateFrom), - ("date", "<=", pms_calendar_search_param.dateTo), - ("pms_property_id", "=", pms_calendar_search_param.pmsPropertyId), - ] - ) - service_lines = self.env["pms.service.line"].search( - [ - ("date", ">=", pms_calendar_search_param.dateFrom), - ("date", "<=", pms_calendar_search_param.dateTo), - ("pms_property_id", "=", pms_calendar_search_param.pmsPropertyId), - ] - ) - 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 ( - date_from + timedelta(d) for d in range((date_to - date_from).days + 1) - ): - reservation_lines_by_day = reservation_lines.filtered( - lambda d: d.date == day + 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, ) - service_lines_by_day = service_lines.filtered(lambda d: d.date == day) result.append( PmsCalendarDailyInvoicing( date=datetime.combine(day, datetime.min.time()).isoformat(), invoicingTotal=round( - sum(reservation_lines_by_day.mapped("price")) - + sum(service_lines_by_day.mapped("price_day_total")), - 2, + (night_production or 0) + (service_production or 0), 2 ), ) ) From ddabf24724377fe20eed2375b2468596ec17efcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 23 Oct 2022 20:53:33 +0200 Subject: [PATCH 210/547] [FIX]pms_api_rest: avoid use obsolete vat_document_type field --- pms_api_rest/services/pms_partner_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 1683dc6ed1..7aac4ded8a 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -124,7 +124,7 @@ def get_partners(self, pms_partner_search_params): if partner.aeat_identification else None, vatDocumentType="02" - if partner.vat_document_type + if partner.vat else partner.aeat_identification_type if partner.aeat_identification_type else None, @@ -444,7 +444,7 @@ def get_partner(self, partner_id): if partner.aeat_identification else None, vatDocumentType="02" - if partner.vat_document_type + if partner.vat else partner.aeat_identification_type if partner.aeat_identification_type else None, From 7ffe23ca85c57653aba53e9e8db298168bef82f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 23 Oct 2022 20:54:11 +0200 Subject: [PATCH 211/547] [IMP]pms_api_rest: get rooms order by sequence --- pms_api_rest/services/pms_room_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 92fe8892d2..1132133f6d 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -75,7 +75,7 @@ def get_rooms(self, room_search_param): .search( domain, ) - .sorted("capacity") + .sorted("sequence") ): result_rooms.append( From 347700e984154019c3d84334c24491d3ca210e9e Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 24 Oct 2022 15:03:11 +0200 Subject: [PATCH 212/547] [IMP] pms_api_rest: add partner type filter in partner list --- pms_api_rest/datamodels/pms_partner.py | 1 + pms_api_rest/services/pms_partner_service.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index 02f3a6dbea..4b0c9e03d8 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -14,6 +14,7 @@ class PmsPartnerSearchParam(Datamodel): name = fields.String(required=False, allow_none=True) housed = 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): diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 7aac4ded8a..54bf754a76 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -39,6 +39,17 @@ def get_partners(self, pms_partner_search_params): .mapped("partner_id") ) domain.append(("id", "in", partners_housed.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: domain.append(("display_name", "ilike", pms_partner_search_params.filter)) if pms_partner_search_params.vatNumberOrName: From 3939d9c9f8ccf4fde63ca4878019fbc4bcc78d39 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 25 Oct 2022 11:48:20 +0200 Subject: [PATCH 213/547] [FIX] pms-api-rest: fix post folio (with preconfirm flag) --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index ba427bf5e7..be647c7eff 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -34,6 +34,7 @@ class PmsFolioInfo(Datamodel): agency = fields.Integer(required=False, allow_none=False) externalReference = fields.String(required=False, allow_none=True) closureReasonId = fields.Integer(required=False, allow_none=True) + preconfirm = fields.Boolean(required=False, allow_none=True) class PmsFolioShortInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ad31111c7d..299f5eb84b 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -369,6 +369,7 @@ def create_folio(self, pms_folio_info): "adults": reservation.adults, "reservation_type": pms_folio_info.reservationType, "children": reservation.children, + "preconfirm": pms_folio_info.preconfirm, } reservation_record = self.env["pms.reservation"].create(vals) if reservation.services: From 8a85736d01dccbb3b0ffb2d8124deed5471afad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 25 Oct 2022 15:34:42 +0200 Subject: [PATCH 214/547] [IMP]pms_api_rest: improvements planning performance --- .../services/pms_availability_plan_service.py | 120 +++++++----- pms_api_rest/services/pms_calendar_service.py | 179 ++++++++++-------- .../services/pms_pricelist_service.py | 80 ++++---- 3 files changed, 209 insertions(+), 170 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 98f117d16e..a6acb68519 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -80,15 +80,21 @@ def get_availability_plans(self, pms_search_param, **args): def get_availability_plan_rules( self, availability_plan_id, availability_plan_rule_search_param ): - result = [] + 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 - PmsAvailabilityPlanRuleInfo = self.env.datamodels[ - "pms.availability.plan.rule.info" - ] + rooms = self.env["pms.room"].search( [ ( @@ -98,50 +104,78 @@ def get_availability_plan_rules( ) ] ) - 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() + 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} + ) - for date in ( - date_from + timedelta(d) for d in range((date_to - date_from).days + 1) - ): - for room_type in self.env["pms.room.type"].search( - [("id", "in", rooms.mapped("room_type_id").ids)] - ): - rule = self.env["pms.availability.plan.rule"].search( - [ - ("date", "=", date), - ( - "availability_plan_id", - "=", - record_availability_plan_id.id, - ), - ("room_type_id", "=", room_type.id), - ( - "pms_property_id", - "=", - availability_plan_rule_search_param.pmsPropertyId, - ), - ] + 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=room_type.id, + 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 if rule.quota != -1 else 0, - maxAvailability=rule.max_avail, - availabilityPlanId=rule.availability_plan_id, + 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"] if rule["quota"] != -1 else 0, + maxAvailability=rule["max_avail"] + if rule["max_avail"] != -1 + else 0, + availabilityPlanId=availability_plan_id, ) result.append(availability_plan_rule_info) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 486dc6a66e..0542c26aa4 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -30,14 +30,33 @@ def get_calendar(self, calendar_search_param): 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", + } + 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( - """ - SELECT night.id as id, night.state, DATE(night.date), night.room_id, - pms_room_type.default_code, reservation.to_assign, reservation.splitted, - reservation.partner_id, reservation.partner_name, folio.id, reservation.id, - reservation.name, reservation.reservation_type, reservation.checkin, - reservation.checkout, reservation.price_total, folio.pending_amount, - reservation.adults + f""" + {sql_select} FROM pms_reservation_line night LEFT JOIN pms_reservation reservation ON reservation.id = night.reservation_id @@ -54,30 +73,12 @@ def get_calendar(self, calendar_search_param): tuple(target_dates), ), ) - SQL_FIELDS = [ - "id", - "state", - "date", - "room_id", - "room_type_name", - "to_assign", - "splitted", - "partner_id", - "partner_name", - "folio_id", - "reservation_id", - "reservation_name", - "reservation_type", - "checkin", - "checkout", - "price_total", - "folio_pending_amount", - "adults", - ] result_sql = self.env.cr.fetchall() lines = [] for res in result_sql: - lines.append({field: res[SQL_FIELDS.index(field)] for field in SQL_FIELDS}) + lines.append( + {field: res[selected_fields.index(field)] for field in selected_fields} + ) PmsCalendarInfo = self.env.datamodels["pms.calendar.info"] result_lines = [] @@ -290,59 +291,64 @@ def get_daily_invoincing(self, pms_calendar_search_param): 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() - result = [] + 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"].search([("room_type_id", "=", room_type.id)]) + ), + } + for room_type in room_types + ] PmsCalendarFreeDailyRoomsByType = self.env.datamodels[ "pms.calendar.free.daily.rooms.by.type" ] - for date in ( - date_from + timedelta(d) for d in range((date_to - date_from).days + 1) - ): - rooms = self.env["pms.room"].search( - [("pms_property_id", "=", pms_calendar_search_param.pmsPropertyId)] - ) - for room_type_iterator in self.env["pms.room.type"].search( - [("id", "in", rooms.mapped("room_type_id").ids)] - ): - reservation_lines_room_type = self.env["pms.reservation.line"].search( - [ - ("date", "=", date), - ("occupies_availability", "=", True), - ("room_id.room_type_id", "=", room_type_iterator.id), - ( - "pms_property_id", - "=", - pms_calendar_search_param.pmsPropertyId, - ), - ] - ) - num_rooms_room_type = self.env["pms.room"].search_count( - [ - ( - "pms_property_id", - "=", - pms_calendar_search_param.pmsPropertyId, - ), - ("room_type_id", "=", room_type_iterator.id), - ] + 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, ) - if not reservation_lines_room_type: - free_rooms_room_type = num_rooms_room_type - else: - free_rooms_room_type = num_rooms_room_type - len( - reservation_lines_room_type - ) result.append( PmsCalendarFreeDailyRoomsByType( date=str( - datetime.combine(date, datetime.min.time()).isoformat() + datetime.combine(day, datetime.min.time()).isoformat() ), - roomTypeId=room_type_iterator.id, - freeRooms=free_rooms_room_type, + roomTypeId=total_room_type["room_type_id"], + freeRooms=total_room_type["rooms_total"] + - count_occupied_night_by_room_type, ) ) return result @@ -361,26 +367,37 @@ def get_free_rooms(self, pms_calendar_search_param): auth="jwt_api_pms", ) def get_alerts_per_day(self, pms_calendar_search_param): - PmsCalendarAlertsPerDay = self.env.datamodels["pms.calendar.alerts.per.day"] 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 ( - date_from + timedelta(d) for d in range((date_to - date_from).days + 1) - ): - lines = self.env["pms.reservation.line"].search_count( - [ - ("date", "=", day), - ("pms_property_id", "=", pms_calendar_search_param.pmsPropertyId), - ("overbooking", "=", True), - ] - ) + for day in target_dates: + overbooking_lines = next((item 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 lines > 0 else False, + overbooking=True if overbooking_lines > 0 else False, ) ) return result diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 4ef9b5cc1d..21061cdd1c 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -89,15 +89,8 @@ def get_pricelists(self, pms_search_param, **args): auth="jwt_api_pms", ) def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): - result = [] - record_pricelist_id = self.env["product.pricelist"].search( - [("id", "=", pricelist_id)] - ) - if not record_pricelist_id: - raise MissingError - PmsPricelistItemInfo = self.env.datamodels["pms.pricelist.item.info"] - rooms = self.env["pms.room"].search( - [("pms_property_id", "=", pricelist_item_search_param.pmsPropertyId)] + pms_property = self.env["pms.property"].browse( + pricelist_item_search_param.pmsPropertyId ) date_from = datetime.strptime( pricelist_item_search_param.dateFrom, "%Y-%m-%d" @@ -105,44 +98,39 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): date_to = datetime.strptime( pricelist_item_search_param.dateTo, "%Y-%m-%d" ).date() - - for date in ( - date_from + timedelta(d) for d in range((date_to - date_from).days + 1) - ): - for room_type in self.env["pms.room.type"].search( - [("id", "in", rooms.mapped("room_type_id").ids)] - ): - item = self.env["product.pricelist.item"].search( - [ - ("pricelist_id", "=", pricelist_id), - ("applied_on", "=", "0_product_variant"), - ("product_id", "=", room_type.product_id.id), - ( - "date_start_consumption", - ">=", - date, - ), - ( - "date_end_consumption", - "<=", - 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"].search( + [("id", "=", pricelist_id)] + ) + if not record_pricelist: + raise MissingError + rooms = self.env["pms.room"].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 item: - pricelist_info = PmsPricelistItemInfo( - roomTypeId=room_type.id, - date=str( - datetime.combine(date, datetime.min.time()).isoformat() - ), - ) - - pricelist_info.pricelistItemId = item[0].id - pricelist_info.price = item[0].fixed_price - - result.append(pricelist_info) - + 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 @restapi.method( From 387c91cb4cbe2f3e0143f247bf49d235382849aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 26 Oct 2022 13:18:06 +0200 Subject: [PATCH 215/547] [FIX]pms_api_rest: count free rooms property domain --- pms_api_rest/services/pms_calendar_service.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 0542c26aa4..097d26ae91 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -322,7 +322,12 @@ def get_free_rooms(self, pms_calendar_search_param): { "room_type_id": room_type.id, "rooms_total": len( - self.env["pms.room"].search([("room_type_id", "=", room_type.id)]) + self.env["pms.room"].search( + [ + ("room_type_id", "=", room_type.id), + ("pms_property_id", "=", pms_property_id), + ] + ) ), } for room_type in room_types From 8d3665b23e62b7999c6bbbd3fd53b79b8124aa25 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 26 Oct 2022 17:33:24 +0200 Subject: [PATCH 216/547] [FIX] pms-api-rest: fix naming agency -> agencyId service folio --- pms_api_rest/datamodels/pms_folio.py | 2 +- pms_api_rest/services/pms_folio_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index be647c7eff..c017a9087d 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -31,7 +31,7 @@ class PmsFolioInfo(Datamodel): ) pricelistId = fields.Integer(required=False, allow_none=False) saleChannelId = fields.Integer(required=False, allow_none=False) - agency = fields.Integer(required=False, allow_none=False) + agencyId = fields.Integer(required=False, allow_none=False) externalReference = fields.String(required=False, allow_none=True) closureReasonId = fields.Integer(required=False, allow_none=True) preconfirm = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 299f5eb84b..9cdd2dd8a8 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -351,7 +351,7 @@ def create_folio(self, pms_folio_info): "pms_property_id": pms_folio_info.pmsPropertyId, "partner_id": pms_folio_info.partnerId, "sale_channel_origin_id": pms_folio_info.saleChannelId, - "agency_id": pms_folio_info.agency, + "agency_id": pms_folio_info.agencyId, "reservation_type": pms_folio_info.reservationType, } folio = self.env["pms.folio"].create(vals) From 83606ef5f23f3d3009e0ff3cf64737b888a0f8fa Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 26 Oct 2022 19:21:06 +0200 Subject: [PATCH 217/547] [FIX] pms-api-rest: remove vat_document_type field --- pms_api_rest/services/pms_partner_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 54bf754a76..8ce5c7955e 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -539,7 +539,6 @@ def mapping_partner_values(self, pms_partner_info): partner_fields.update( { "vat": pms_partner_info.vatNumber, - "vat_document_type": "vat", } ) else: From 55759fff1b3163d4ed91f6a19a761ded0f11577e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 26 Oct 2022 19:46:58 +0200 Subject: [PATCH 218/547] [FIX]pms_api_rest: update services with 0 value (is not none to check value) --- pms_api_rest/services/pms_service_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py index 5a48913ad6..5b27914383 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -107,15 +107,15 @@ 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 and ( + 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 and ( + 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 and ( + 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 From c3477383a7ae1f1f7d9953f5d04d10ef195ea5c7 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 26 Oct 2022 10:24:53 +0200 Subject: [PATCH 219/547] [IMP] pms-api-rest: change swap behaviour, now swap force splitteds if required --- pms_api_rest/datamodels/pms_calendar.py | 10 ++-- pms_api_rest/services/pms_calendar_service.py | 54 ++++++++++--------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index e840d639c4..ba4e336d96 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -1,6 +1,7 @@ from marshmallow import fields from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel class PmsReservationUpdates(Datamodel): @@ -16,11 +17,10 @@ class PmsReservationUpdates(Datamodel): class PmsCalendarSwapInfo(Datamodel): _name = "pms.calendar.swap.info" - swapFrom = fields.String(required=True, allow_none=False) - swapTo = fields.String(required=True, allow_none=False) - roomIdA = fields.Integer(required=True, allow_none=False) - roomIdB = fields.Integer(required=True, allow_none=False) - pmsPropertyId = fields.Integer(required=False, allow_none=True) + 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): diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 097d26ae91..ccd1271fca 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -158,35 +158,37 @@ def get_calendar(self, calendar_search_param): auth="jwt_api_pms", ) def swap_reservation_slices(self, swap_info): - room_id_a = swap_info.roomIdA - room_id_b = swap_info.roomIdB - - lines_room_a = self.env["pms.reservation.line"].search( + reservation_lines_target = self.env['pms.reservation.line'].search( [ - ("room_id", "=", room_id_a), - ("date", ">=", swap_info.swapFrom), - ("date", "<=", swap_info.swapTo), - ("pms_property_id", "=", swap_info.pmsPropertyId), + ("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.occupies_availability = False + affected_line.occupies_availability = False + + reservation_line.flush() + affected_line.flush() + + reservation_line.room_id = swap_info.roomId + affected_line.room_id = old_room_id + + reservation_line.occupies_availability = True + affected_line.occupies_availability = True + + reservation_line._compute_occupies_availability() + affected_line._compute_occupies_availability() + + - lines_room_b = self.env["pms.reservation.line"].search( - [ - ("room_id", "=", room_id_b), - ("date", ">=", swap_info.swapFrom), - ("date", "<=", swap_info.swapTo), - ("pms_property_id", "=", swap_info.pmsPropertyId), - ] - ) - lines_room_a.occupies_availability = False - lines_room_b.occupies_availability = False - lines_room_a.flush() - lines_room_b.flush() - lines_room_a.room_id = room_id_b - lines_room_b.room_id = room_id_a - - lines_room_a._compute_occupies_availability() - lines_room_b._compute_occupies_availability() @restapi.method( [ From 3ee9ecf3536bdb9dd9c9c2564f76fad6206f1ba0 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 26 Oct 2022 10:28:10 +0200 Subject: [PATCH 220/547] [FIX] pms-api-rest: fix precommit --- pms_api_rest/datamodels/pms_calendar.py | 1 - pms_api_rest/services/pms_calendar_service.py | 13 +++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index ba4e336d96..77ce987499 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -1,7 +1,6 @@ from marshmallow import fields from odoo.addons.datamodel.core import Datamodel -from odoo.addons.datamodel.fields import NestedModel class PmsReservationUpdates(Datamodel): diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index ccd1271fca..3fb4987de8 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -158,11 +158,11 @@ def get_calendar(self, calendar_search_param): 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) + 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 @@ -187,9 +187,6 @@ def swap_reservation_slices(self, swap_info): reservation_line._compute_occupies_availability() affected_line._compute_occupies_availability() - - - @restapi.method( [ ( From 5b194833217ce3fb7fd462d5a8655ff81caa7af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 26 Oct 2022 20:08:34 +0200 Subject: [PATCH 221/547] [TMP]pms_api_rest: avoid drag&drop change reservation dates --- pms_api_rest/services/pms_calendar_service.py | 104 ++++++++++-------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 3fb4987de8..43ebcf0da7 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -421,57 +421,67 @@ def get_alerts_per_day(self, pms_calendar_search_param): def update_reservation(self, reservation_id, reservation_lines_changes): if reservation_lines_changes.reservationLinesChanges: - # get date of first reservation id to change - first_reservation_line_id_to_change = ( - reservation_lines_changes.reservationLinesChanges[0][ - "reservationLineId" + # 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 ] ) - 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" - ) + 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"] + # # 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 + # 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 = ( From bdbd2f150480ca387ae47ffe69fb91afb04c2c9f Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 26 Oct 2022 20:34:15 +0200 Subject: [PATCH 222/547] [IMP]pms_api_rest: added return checkin_partner id in checkin_partner PATCH/POST --- pms_api_rest/services/pms_reservation_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index ac0ea8a9ed..72b61fae9f 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -565,6 +565,7 @@ def write_reservation_checkin_partner( checkin_partner.write( self.mapping_checkin_partner_values(pms_checkin_partner_info) ) + return checkin_partner.id @restapi.method( [ @@ -666,6 +667,7 @@ def create_reservation_checkin_partner( checkin_partner.write( self.mapping_checkin_partner_values(pms_checkin_partner_info) ) + return checkin_partner.id @restapi.method( [ From acd318d6e843a263906011f846d23eee1f84592d Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 27 Oct 2022 15:21:57 +0200 Subject: [PATCH 223/547] [IMP]pms_api_rest: added partner_name, phone and mobile in folio POST service --- pms_api_rest/services/pms_folio_service.py | 26 +++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 9cdd2dd8a8..f895b8c898 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -349,11 +349,35 @@ def create_folio(self, pms_folio_info): else: vals = { "pms_property_id": pms_folio_info.pmsPropertyId, - "partner_id": pms_folio_info.partnerId, "sale_channel_origin_id": pms_folio_info.saleChannelId, "agency_id": pms_folio_info.agencyId, "reservation_type": pms_folio_info.reservationType, } + 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 = { From 9224cba219480dabf72973ea76ddff931328b062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 30 Oct 2022 11:56:26 +0100 Subject: [PATCH 224/547] [FIX]pms_api_rest: mapper agencyId 0 to False in ORM --- pms_api_rest/services/pms_folio_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index f895b8c898..5fb0128d33 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -350,7 +350,9 @@ def create_folio(self, pms_folio_info): vals = { "pms_property_id": pms_folio_info.pmsPropertyId, "sale_channel_origin_id": pms_folio_info.saleChannelId, - "agency_id": pms_folio_info.agencyId, + "agency_id": pms_folio_info.agencyId + if pms_folio_info.agencyId + else False, "reservation_type": pms_folio_info.reservationType, } if pms_folio_info.partnerId: From c7792fb7d2a37b20721ec56b7f63264526dbe36d Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 2 Nov 2022 19:49:57 +0100 Subject: [PATCH 225/547] [IMP] pms-api-rest: res. lines add price_day_total & price_day_total_services --- pms_api_rest/datamodels/pms_calendar.py | 3 ++- pms_api_rest/services/pms_calendar_service.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 77ce987499..cb3575313d 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -68,7 +68,8 @@ class PmsCalendarInfo(Datamodel): nextLineSplitted = fields.Boolean(required=False, allow_none=True) previousLineSplitted = fields.Boolean(required=False, allow_none=True) closureReason = fields.String(required=False, allow_none=True) - + priceDayTotal = fields.Number(required=False, allow_none=True) + priceDayTotalServices = fields.Number(required=False, allow_none=True) class PmsCalendarAlertsPerDay(Datamodel): _name = "pms.calendar.alerts.per.day" diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 43ebcf0da7..3222318b81 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -30,7 +30,12 @@ def get_calendar(self, calendar_search_param): 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 - + subselect_sum_services_price = "(" \ + " SELECT COALESCE(SUM(s.price_day_total),0) price_day_total_services " \ + " FROM pms_service_line s " \ + " WHERE s.reservation_id = night.reservation_id " \ + " AND s.date = night.date AND NOT s.is_board_service " \ + " ) " selected_fields_mapper = { "id": "night.id", "state": "night.state", @@ -50,6 +55,8 @@ def get_calendar(self, calendar_search_param): "price_total": "reservation.price_total", "folio_pending_amount": "folio.pending_amount", "adults": "reservation.adults", + "price_day_total": "night.price_day_total", + "price_day_total_services": subselect_sum_services_price } selected_fields_sql = list(selected_fields_mapper.values()) selected_fields = list(selected_fields_mapper.keys()) @@ -132,6 +139,8 @@ def get_calendar(self, calendar_search_param): 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=round(line["price_day_total_services"], 0), # TODO: line.reservation_id.message_needaction_counter is computed field, numNotifications=0, adults=line["adults"], From f22b5cefa57330007c3f47c399498982cace3333 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 2 Nov 2022 19:51:07 +0100 Subject: [PATCH 226/547] [FIX] pms-api-rest: fix precommit --- pms_api_rest/datamodels/pms_calendar.py | 1 + pms_api_rest/services/pms_calendar_service.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index cb3575313d..9e54796ae1 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -71,6 +71,7 @@ class PmsCalendarInfo(Datamodel): priceDayTotal = fields.Number(required=False, allow_none=True) priceDayTotalServices = fields.Number(required=False, allow_none=True) + class PmsCalendarAlertsPerDay(Datamodel): _name = "pms.calendar.alerts.per.day" date = fields.String(required=True, allow_none=False) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 3222318b81..f4476dd489 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -30,12 +30,14 @@ def get_calendar(self, calendar_search_param): 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 - subselect_sum_services_price = "(" \ - " SELECT COALESCE(SUM(s.price_day_total),0) price_day_total_services " \ - " FROM pms_service_line s " \ - " WHERE s.reservation_id = night.reservation_id " \ - " AND s.date = night.date AND NOT s.is_board_service " \ - " ) " + subselect_sum_services_price = ( + "(" + " SELECT COALESCE(SUM(s.price_day_total),0) price_day_total_services " + " FROM pms_service_line s " + " WHERE s.reservation_id = night.reservation_id " + " AND s.date = night.date AND NOT s.is_board_service " + " ) " + ) selected_fields_mapper = { "id": "night.id", "state": "night.state", @@ -56,7 +58,7 @@ def get_calendar(self, calendar_search_param): "folio_pending_amount": "folio.pending_amount", "adults": "reservation.adults", "price_day_total": "night.price_day_total", - "price_day_total_services": subselect_sum_services_price + "price_day_total_services": subselect_sum_services_price, } selected_fields_sql = list(selected_fields_mapper.values()) selected_fields = list(selected_fields_mapper.keys()) From 54157e9eb71edbff04def79465956c04bd7fde00 Mon Sep 17 00:00:00 2001 From: braisab Date: Sat, 29 Oct 2022 00:11:59 +0200 Subject: [PATCH 227/547] [IMP]pms_api_rest: added GET services for sale lines and invoices --- pms_api_rest/datamodels/__init__.py | 2 + pms_api_rest/datamodels/pms_account_move.py | 5 +- .../datamodels/pms_folio_sale_line.py | 17 +++ pms_api_rest/datamodels/pms_invoice_line.py | 12 ++ pms_api_rest/services/pms_folio_service.py | 115 ++++++++++++++++++ 5 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 pms_api_rest/datamodels/pms_folio_sale_line.py create mode 100644 pms_api_rest/datamodels/pms_invoice_line.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 4cb78ab875..235fdeb3fc 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -52,3 +52,5 @@ from . import pms_room_closure_reason from . import pms_report +from . import pms_folio_sale_line +from . import pms_invoice_line diff --git a/pms_api_rest/datamodels/pms_account_move.py b/pms_api_rest/datamodels/pms_account_move.py index 8b47fe8819..a09d8edc86 100644 --- a/pms_api_rest/datamodels/pms_account_move.py +++ b/pms_api_rest/datamodels/pms_account_move.py @@ -1,9 +1,10 @@ from marshmallow import fields from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel -class PmsAccountMove(Datamodel): +class PmsAccountMoveInfo(Datamodel): _name = "pms.account.move.info" id = fields.Integer(required=False, allow_none=True) amount = fields.Float(required=False, allow_none=True) @@ -11,3 +12,5 @@ class PmsAccountMove(Datamodel): date = fields.String(required=False, allow_none=True) paymentState = fields.String(required=False, allow_none=True) state = fields.String(required=False, allow_none=True) + partnerName = fields.String(required=False, allow_none=True) + moveLines = fields.List(NestedModel("pms.invoice.line.info")) 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..89c5837e60 --- /dev/null +++ b/pms_api_rest/datamodels/pms_folio_sale_line.py @@ -0,0 +1,17 @@ +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) + 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) 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..e4a44f2439 --- /dev/null +++ b/pms_api_rest/datamodels/pms_invoice_line.py @@ -0,0 +1,12 @@ +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) + total = fields.Float(required=False, allow_none=True) + displayType = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 5fb0128d33..a46e4c85bf 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -421,3 +421,118 @@ def create_folio(self, pms_folio_info): self.env["pms.service"].create(vals) return folio.id + + @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=sale_line.qty_to_invoice + if sale_line.qty_to_invoice + else None, + qtyInvoiced=sale_line.qty_invoiced + if sale_line.qty_invoiced + else None, + priceTotal=sale_line.price_total + if sale_line.price_total + 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, + ) + ) + + return sale_lines + + @restapi.method( + [ + ( + [ + "//invoices", + ], + "GET", + ) + ], + output_param=Datamodel("pms.account.move.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.account.move.info"] + PmsInvoiceLineInfo = self.env.datamodels["pms.invoice.line.info"] + if folio.move_ids: + for move_id in folio.move_ids: + move_lines = [] + for move_line in move_id.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, + total=move_line.price_total + if move_line.price_total + else None, + displayType=move_line.display_type + if move_line.display_type + else None, + ) + ) + invoices.append( + PmsFolioInvoiceInfo( + id=move_id.id if move_id.id else None, + name=move_id.name if move_id.name else None, + amount=round(move_id.amount_total, 2) + if move_id.amount_total + else None, + date=move_id.date.strftime("%d/%m/%Y") + if move_id.date + else None, + state=move_id.state if move_id.state else None, + paymentState=move_id.payment_state + if move_id.payment_state + else None, + partnerName=move_id.partner_id.name + if move_id.partner_id.name + else None, + moveLines=move_lines if move_lines else None, + ) + ) + return invoices From 42a0b63a391a7b8926ce26a927896f26be34af51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 30 Oct 2022 07:44:40 +0100 Subject: [PATCH 228/547] [ADD]pms_api_rest: invoicing folios services --- pms_api_rest/datamodels/__init__.py | 2 +- .../datamodels/pms_account_payment.py | 9 -- .../{pms_account_move.py => pms_invoice.py} | 7 +- pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/pms_folio_service.py | 52 ++++++++- pms_api_rest/services/pms_invoice_service.py | 105 ++++++++++++++++++ pms_api_rest/services/pms_partner_service.py | 4 +- 7 files changed, 160 insertions(+), 20 deletions(-) rename pms_api_rest/datamodels/{pms_account_move.py => pms_invoice.py} (71%) create mode 100644 pms_api_rest/services/pms_invoice_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 235fdeb3fc..9128d5a9d2 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -15,7 +15,7 @@ from . import pms_property from . import pms_account_journal from . import pms_account_payment -from . import pms_account_move +from . import pms_invoice from . import pms_user diff --git a/pms_api_rest/datamodels/pms_account_payment.py b/pms_api_rest/datamodels/pms_account_payment.py index 45025b669d..fbf96adcbc 100644 --- a/pms_api_rest/datamodels/pms_account_payment.py +++ b/pms_api_rest/datamodels/pms_account_payment.py @@ -11,14 +11,5 @@ class PmsPaymentInfo(Datamodel): date = fields.String(required=False, allow_none=True) paymentType = fields.String(required=False, allow_none=True) reference = fields.String(required=False, allow_none=True) - - -class PmsAccountPaymentInfo(Datamodel): - _name = "pms.account.payment.short.info" - id = fields.Integer(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) - reference = fields.String(required=False, allow_none=True) reservationIds = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/datamodels/pms_account_move.py b/pms_api_rest/datamodels/pms_invoice.py similarity index 71% rename from pms_api_rest/datamodels/pms_account_move.py rename to pms_api_rest/datamodels/pms_invoice.py index a09d8edc86..b09fa74a19 100644 --- a/pms_api_rest/datamodels/pms_account_move.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -4,13 +4,16 @@ from odoo.addons.datamodel.fields import NestedModel -class PmsAccountMoveInfo(Datamodel): - _name = "pms.account.move.info" +class PmsAccountInvoiceInfo(Datamodel): + _name = "pms.account.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) + # partnerName??, is not enought partnerId? partnerName = fields.String(required=False, allow_none=True) + partnerId = fields.Int(required=False, allow_none=True) moveLines = fields.List(NestedModel("pms.invoice.line.info")) + saleLines = fields.List(NestedModel("pms.folio.sale.line.info")) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 2aa9295969..f38e8f73d8 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -37,3 +37,4 @@ from . import res_lang_service from . import pms_account_payment_terms_service from . import pms_account_journal_service +from . import pms_invoice_service diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index a46e4c85bf..a97d930ae4 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -214,7 +214,7 @@ def get_folio_payments(self, folio_id, pms_search_param): "POST", ) ], - input_param=Datamodel("pms.account.payment.short.info", is_list=False), + input_param=Datamodel("pms.account.payment.info", is_list=False), auth="jwt_api_pms", ) def create_folio_charge(self, folio_id, pms_account_payment_info): @@ -244,7 +244,7 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): "POST", ) ], - input_param=Datamodel("pms.account.payment.short.info", is_list=False), + input_param=Datamodel("pms.account.payment.info", is_list=False), auth="jwt_api_pms", ) def create_folio_refund(self, folio_id, pms_account_payment_info): @@ -485,7 +485,7 @@ def get_folio_sale_lines(self, folio_id): "GET", ) ], - output_param=Datamodel("pms.account.move.info", is_list=True), + output_param=Datamodel("pms.account.info", is_list=True), auth="jwt_api_pms", ) def get_folio_invoices(self, folio_id): @@ -494,7 +494,7 @@ def get_folio_invoices(self, folio_id): if not folio: pass else: - PmsFolioInvoiceInfo = self.env.datamodels["pms.account.move.info"] + PmsFolioInvoiceInfo = self.env.datamodels["pms.account.info"] PmsInvoiceLineInfo = self.env.datamodels["pms.invoice.line.info"] if folio.move_ids: for move_id in folio.move_ids: @@ -522,8 +522,8 @@ def get_folio_invoices(self, folio_id): amount=round(move_id.amount_total, 2) if move_id.amount_total else None, - date=move_id.date.strftime("%d/%m/%Y") - if move_id.date + date=move_id.invoice_date.strftime("%d/%m/%Y") + if move_id.invoice_date else None, state=move_id.state if move_id.state else None, paymentState=move_id.payment_state @@ -536,3 +536,43 @@ def get_folio_invoices(self, folio_id): ) ) return invoices + + @restapi.method( + [ + ( + [ + "//invoices", + ], + "POST", + ) + ], + input_param=Datamodel("pms.account.info", is_list=False), + auth="jwt_api_pms", + ) + def create_folio_invoices(self, folio_id, invoice_info): + # TODO: Missing payload data: + # - partnerId selected + # - quantity to invoice selected + # - front line description modification + # - data format mal formartted + # - invoice comment + + # date_invoice = fields.Date.from_string(invoice_info.date) + # if not date_invoice: + # raise MissingError(_("Date is required")) + lines_to_invoice_dict = dict() + for item in invoice_info.saleLines: + # TODO: Need get specific to_invoice front value + 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() + ) + folios_to_invoice = sale_lines_to_invoice.folio_id + invoices = folios_to_invoice._create_invoices( + # date=date_invoice, TODO: Wrong format date from front + lines_to_invoice=lines_to_invoice_dict, + partner_invoice_id=105165, + ) + return invoices.ids 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..8149f11deb --- /dev/null +++ b/pms_api_rest/services/pms_invoice_service.py @@ -0,0 +1,105 @@ +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 PmsInvoiceService(Component): + _inherit = "base.rest.service" + _name = "pms.room.service" + _usage = "invoices" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.invoice.info"), + auth="jwt_api_pms", + ) + def update_invoice(self, invoice_id, pms_invoice_info): + invoice = self.env["account.move"].browse(invoice_id) + company = invoice.company_id + + # Build update values dict + # TODO: Missing data: + # - invoice comment (narration) + # - add new invoice lines (from saleLines selected?) + + 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: + 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 and pms_invoice_info.moveLines is not None: + new_vals["reservation_line_ids"] = [] + for line in invoice.invoice_line_ids: + line_info = [ + item.id for item in pms_invoice_info.moveLines if item.id == line.id + ] + if line_info: + line_values = {} + if line_info.name and line_info.name != line.name: + line_values["name"] = line_info.name + if line_info.quantity and line_info.quantity != line.quantity: + line_values["quantity"] = line_info.quantity + new_vals["reservation_line_ids"].append((1, 4, line_values)) + else: + new_vals["reservation_line_ids"].append((2, line.id)) + + if not new_vals: + return invoice.id + + # Update Invoice + # When modifying an invoice, depending on the company's configuration, + # and the invoice stateit will be modified directly or a reverse + # of the current invoice will be created to later create a new one + # with the updated data. + if invoice.state != "draft" and company.corrective_invoice_policy == "strict": + # invoice create refund + # new invoice with new_vals + move_reversal = ( + self.env["account.move.reversal"] + .with_context(active_model="account.move", active_ids=invoice.ids) + .create( + { + "date": fields.Date.today() + datetime.timedelta(days=7), + "reason": _("Invoice modification"), + "refund_method": "modify", + } + ) + ) + reversal_action = move_reversal.reverse_moves() + reverse_invoice = self.env["account.move"].browse(reversal_action["res_id"]) + invoice = reverse_invoice + + invoice = self._direct_move_update(invoice, new_vals) + return invoice.id + + def _direct_move_update(self, invoice, new_vals): + previus_state = invoice.state + if previus_state == "posted": + invoice.button_draft() + if new_vals: + invoice.write(new_vals) + if previus_state == "posted": + invoice.action_post() + return invoice diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 8ce5c7955e..f75c60a733 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -246,7 +246,7 @@ def get_partner_payments(self, partner_id): "GET", ) ], - output_param=Datamodel("pms.account.move.info", is_list=True), + output_param=Datamodel("pms.account.info", is_list=True), auth="jwt_api_pms", ) def get_partner_invoices(self, partner_id): @@ -256,7 +256,7 @@ def get_partner_invoices(self, partner_id): ("move_type", "in", self.env["account.move"].get_invoice_types()), ] ) - PmsAcoountMoveInfo = self.env.datamodels["pms.account.move.info"] + PmsAcoountMoveInfo = self.env.datamodels["pms.account.info"] invoices = [] for invoice in partnerInvoices: invoices.append( From 6f6c739f410a16d903031e09b42c908add3a27a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 31 Oct 2022 09:51:49 +0100 Subject: [PATCH 229/547] [TMP]pms_api_rest: waiting for new company corrective invoicing policy field --- pms_api_rest/services/pms_invoice_service.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 8149f11deb..6588fcd792 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -1,5 +1,3 @@ -from datetime import datetime - from odoo import _, fields from odoo.addons.base_rest import restapi @@ -27,7 +25,6 @@ class PmsInvoiceService(Component): ) def update_invoice(self, invoice_id, pms_invoice_info): invoice = self.env["account.move"].browse(invoice_id) - company = invoice.company_id # Build update values dict # TODO: Missing data: @@ -73,7 +70,9 @@ def update_invoice(self, invoice_id, pms_invoice_info): # and the invoice stateit will be modified directly or a reverse # of the current invoice will be created to later create a new one # with the updated data. - if invoice.state != "draft" and company.corrective_invoice_policy == "strict": + # TODO: to create core pms correct_invoice_policy field + # if invoice.state != "draft" and company.corrective_invoice_policy == "strict": + if invoice.state != "draft": # invoice create refund # new invoice with new_vals move_reversal = ( @@ -81,7 +80,7 @@ def update_invoice(self, invoice_id, pms_invoice_info): .with_context(active_model="account.move", active_ids=invoice.ids) .create( { - "date": fields.Date.today() + datetime.timedelta(days=7), + "date": fields.Date.today(), "reason": _("Invoice modification"), "refund_method": "modify", } From 2ce4a6a813639db44edd297591d2e53bf48676f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 1 Nov 2022 11:54:18 +0100 Subject: [PATCH 230/547] [FIX]pms_api_rest: fix service invoice naming --- pms_api_rest/services/pms_invoice_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 6588fcd792..30c8fbe60f 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -7,7 +7,7 @@ class PmsInvoiceService(Component): _inherit = "base.rest.service" - _name = "pms.room.service" + _name = "pms.invoice" _usage = "invoices" _collection = "pms.services" From b88246a66c1ec6e19a7a46f21440b225315015ee Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 2 Nov 2022 19:10:08 +0100 Subject: [PATCH 231/547] [FIX]14.0-pms_api_rest: services and datamodels for create/update invoices --- pms_api_rest/datamodels/pms_invoice.py | 8 ++- pms_api_rest/datamodels/pms_invoice_line.py | 2 + pms_api_rest/services/pms_folio_service.py | 61 +++++++++++--------- pms_api_rest/services/pms_invoice_service.py | 11 ++-- pms_api_rest/services/pms_partner_service.py | 4 +- 5 files changed, 49 insertions(+), 37 deletions(-) diff --git a/pms_api_rest/datamodels/pms_invoice.py b/pms_api_rest/datamodels/pms_invoice.py index b09fa74a19..ab5e0779fc 100644 --- a/pms_api_rest/datamodels/pms_invoice.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -5,15 +5,17 @@ class PmsAccountInvoiceInfo(Datamodel): - _name = "pms.account.info" + _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) - # partnerName??, is not enought partnerId? + # REVIEW: partnerName??, is not enought partnerId? partnerName = fields.String(required=False, allow_none=True) - partnerId = fields.Int(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) moveLines = fields.List(NestedModel("pms.invoice.line.info")) 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) diff --git a/pms_api_rest/datamodels/pms_invoice_line.py b/pms_api_rest/datamodels/pms_invoice_line.py index e4a44f2439..29aa784ecd 100644 --- a/pms_api_rest/datamodels/pms_invoice_line.py +++ b/pms_api_rest/datamodels/pms_invoice_line.py @@ -8,5 +8,7 @@ class PmsInvoiceLineInfo(Datamodel): 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) displayType = fields.String(required=False, allow_none=True) + saleLineId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index a97d930ae4..c1b9448bbe 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -485,7 +485,7 @@ def get_folio_sale_lines(self, folio_id): "GET", ) ], - output_param=Datamodel("pms.account.info", is_list=True), + output_param=Datamodel("pms.invoice.info", is_list=True), auth="jwt_api_pms", ) def get_folio_invoices(self, folio_id): @@ -494,12 +494,12 @@ def get_folio_invoices(self, folio_id): if not folio: pass else: - PmsFolioInvoiceInfo = self.env.datamodels["pms.account.info"] + PmsFolioInvoiceInfo = self.env.datamodels["pms.invoice.info"] PmsInvoiceLineInfo = self.env.datamodels["pms.invoice.line.info"] if folio.move_ids: - for move_id in folio.move_ids: + for move in folio.move_ids: move_lines = [] - for move_line in move_id.invoice_line_ids: + for move_line in move.invoice_line_ids: move_lines.append( PmsInvoiceLineInfo( id=move_line.id, @@ -507,6 +507,9 @@ def get_folio_invoices(self, folio_id): 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, @@ -515,24 +518,32 @@ def get_folio_invoices(self, folio_id): else None, ) ) + portal_url = ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url") + + move.get_portal_url() + ) invoices.append( PmsFolioInvoiceInfo( - id=move_id.id if move_id.id else None, - name=move_id.name if move_id.name else None, - amount=round(move_id.amount_total, 2) - if move_id.amount_total + 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=move.invoice_date.strftime("%d/%m/%Y") + if move.invoice_date else None, - date=move_id.invoice_date.strftime("%d/%m/%Y") - if move_id.invoice_date + state=move.state if move.state else None, + paymentState=move.payment_state + if move.payment_state else None, - state=move_id.state if move_id.state else None, - paymentState=move_id.payment_state - if move_id.payment_state + partnerName=move.partner_id.name + if move.partner_id.name else None, - partnerName=move_id.partner_id.name - if move_id.partner_id.name + partnerId=move.partner_id.id + if move.partner_id.id else None, moveLines=move_lines if move_lines else None, + portalUrl=portal_url, ) ) return invoices @@ -546,23 +557,19 @@ def get_folio_invoices(self, folio_id): "POST", ) ], - input_param=Datamodel("pms.account.info", is_list=False), + 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: - # - partnerId selected - # - quantity to invoice selected - # - front line description modification - # - data format mal formartted - # - invoice comment + # - date format is in invoice_info but dont save + # - invoice comment is in invoice_info but dont save - # date_invoice = fields.Date.from_string(invoice_info.date) - # if not date_invoice: - # raise MissingError(_("Date is required")) + date_invoice = fields.Date.from_string(invoice_info.date) + if not date_invoice: + raise MissingError(_("Date is required")) lines_to_invoice_dict = dict() for item in invoice_info.saleLines: - # TODO: Need get specific to_invoice front value if item.qtyToInvoice: lines_to_invoice_dict[item.id] = item.qtyToInvoice @@ -571,8 +578,8 @@ def create_folio_invoices(self, folio_id, invoice_info): ) folios_to_invoice = sale_lines_to_invoice.folio_id invoices = folios_to_invoice._create_invoices( - # date=date_invoice, TODO: Wrong format date from front + date=date_invoice, lines_to_invoice=lines_to_invoice_dict, - partner_invoice_id=105165, + partner_invoice_id=invoice_info.partnerId, ) return invoices.ids diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 30c8fbe60f..34d13ad96a 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -7,7 +7,7 @@ class PmsInvoiceService(Component): _inherit = "base.rest.service" - _name = "pms.invoice" + _name = "pms.invoice.service" _usage = "invoices" _collection = "pms.services" @@ -47,20 +47,21 @@ def update_invoice(self, invoice_id, pms_invoice_info): # send to service, the lines that are not sent we assume that # they have been eliminated if pms_invoice_info.moveLines and pms_invoice_info.moveLines is not None: - new_vals["reservation_line_ids"] = [] + new_vals["invoice_line_ids"] = [] for line in invoice.invoice_line_ids: line_info = [ - item.id for item in pms_invoice_info.moveLines if item.id == line.id + 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 and line_info.name != line.name: line_values["name"] = line_info.name if line_info.quantity and line_info.quantity != line.quantity: line_values["quantity"] = line_info.quantity - new_vals["reservation_line_ids"].append((1, 4, line_values)) + new_vals["invoice_line_ids"].append((1, line.id, line_values)) else: - new_vals["reservation_line_ids"].append((2, line.id)) + new_vals["invoice_line_ids"].append((2, line.id)) if not new_vals: return invoice.id diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index f75c60a733..08cff7a6a2 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -246,7 +246,7 @@ def get_partner_payments(self, partner_id): "GET", ) ], - output_param=Datamodel("pms.account.info", is_list=True), + output_param=Datamodel("pms.invoice.info", is_list=True), auth="jwt_api_pms", ) def get_partner_invoices(self, partner_id): @@ -256,7 +256,7 @@ def get_partner_invoices(self, partner_id): ("move_type", "in", self.env["account.move"].get_invoice_types()), ] ) - PmsAcoountMoveInfo = self.env.datamodels["pms.account.info"] + PmsAcoountMoveInfo = self.env.datamodels["pms.invoice.info"] invoices = [] for invoice in partnerInvoices: invoices.append( From 69e51cc13954d585977e3157fd0d2a9810594b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 3 Nov 2022 09:37:41 +0100 Subject: [PATCH 232/547] [ADD]pms_api_rest: datamodel payment info & add fields to invoice datamodel --- pms_api_rest/services/pms_folio_service.py | 20 +++++++++++++++++--- pms_api_rest/services/pms_invoice_service.py | 13 +++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index c1b9448bbe..d4f2598e38 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -214,7 +214,7 @@ def get_folio_payments(self, folio_id, pms_search_param): "POST", ) ], - input_param=Datamodel("pms.account.payment.info", is_list=False), + input_param=Datamodel("pms.payment.info", is_list=False), auth="jwt_api_pms", ) def create_folio_charge(self, folio_id, pms_account_payment_info): @@ -223,13 +223,18 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): journal_id = 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 + ) self.env["pms.folio"].do_payment( journal_id, journal_id.suspense_account_id, self.env.user, pms_account_payment_info.amount, folio, - reservations=pms_account_payment_info.reservationIds, + reservations=reservations, services=False, partner=partner_id, date=datetime.strptime(pms_account_payment_info.date, "%m/%d/%Y"), @@ -244,7 +249,7 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): "POST", ) ], - input_param=Datamodel("pms.account.payment.info", is_list=False), + input_param=Datamodel("pms.payment.info", is_list=False), auth="jwt_api_pms", ) def create_folio_refund(self, folio_id, pms_account_payment_info): @@ -582,4 +587,13 @@ def create_folio_invoices(self, folio_id, invoice_info): lines_to_invoice=lines_to_invoice_dict, partner_invoice_id=invoice_info.partnerId, ) + 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 + ) + invoice_line.write({"name": item.name}) + if invoice_info.narration: + invoices.write({"narration": invoice_info.narration}) + return invoices.ids diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 34d13ad96a..a5e153d0ba 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -62,6 +62,19 @@ def update_invoice(self, invoice_id, pms_invoice_info): new_vals["invoice_line_ids"].append((1, line.id, line_values)) else: new_vals["invoice_line_ids"].append((2, line.id)) + for line_info in pms_invoice_info.moveLines: + if not line_info.id: + new_vals["invoice_line_ids"].append( + ( + 0, + 0, + { + "name": line_info.name, + "quantity": line_info.quantity, + "sale_line_ids": [(6, 0, line_info.saleLineIds)], + }, + ) + ) if not new_vals: return invoice.id From cbcba6607a7ea2a47765dcde2f3d96eb56408696 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 3 Nov 2022 14:38:21 +0100 Subject: [PATCH 233/547] [IMP]pms_api_rest: added sale_line_id to invoice line GET service --- pms_api_rest/services/pms_folio_service.py | 3 +++ pms_api_rest/services/pms_invoice_service.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d4f2598e38..2c5dc963de 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -521,6 +521,9 @@ def get_folio_invoices(self, folio_id): displayType=move_line.display_type if move_line.display_type else None, + saleLineId=move_line.folio_line_ids + if move_line.folio_line_ids + else None, ) ) portal_url = ( diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index a5e153d0ba..ece8c30e84 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -71,7 +71,7 @@ def update_invoice(self, invoice_id, pms_invoice_info): { "name": line_info.name, "quantity": line_info.quantity, - "sale_line_ids": [(6, 0, line_info.saleLineIds)], + "sale_line_ids": [(6, 0, line_info.saleLineId)], }, ) ) From 6951f3fe6971854390b9cdd5c09e1821b4747c66 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 4 Nov 2022 18:42:20 +0100 Subject: [PATCH 234/547] [IMP]pms_api_rest: added datamodel and service header to mail invoice --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_mail.py | 11 +++++++++++ pms_api_rest/services/pms_folio_service.py | 4 ---- pms_api_rest/services/pms_invoice_service.py | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_mail.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 9128d5a9d2..186d4d7bfa 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -54,3 +54,4 @@ from . import pms_report from . import pms_folio_sale_line from . import pms_invoice_line +from . import pms_mail diff --git a/pms_api_rest/datamodels/pms_mail.py b/pms_api_rest/datamodels/pms_mail.py new file mode 100644 index 0000000000..211335834f --- /dev/null +++ b/pms_api_rest/datamodels/pms_mail.py @@ -0,0 +1,11 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsMailInfo(Datamodel): + _name = "pms.mail.info" + 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/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 2c5dc963de..1dc4095210 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -573,9 +573,6 @@ def create_folio_invoices(self, folio_id, invoice_info): # - date format is in invoice_info but dont save # - invoice comment is in invoice_info but dont save - date_invoice = fields.Date.from_string(invoice_info.date) - if not date_invoice: - raise MissingError(_("Date is required")) lines_to_invoice_dict = dict() for item in invoice_info.saleLines: if item.qtyToInvoice: @@ -586,7 +583,6 @@ def create_folio_invoices(self, folio_id, invoice_info): ) folios_to_invoice = sale_lines_to_invoice.folio_id invoices = folios_to_invoice._create_invoices( - date=date_invoice, lines_to_invoice=lines_to_invoice_dict, partner_invoice_id=invoice_info.partnerId, ) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index ece8c30e84..4412f313da 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -116,3 +116,18 @@ def _direct_move_update(self, invoice, new_vals): if previus_state == "posted": invoice.action_post() return invoice + + @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): + return True From 0ab1ead5e190974c3bdf9e3b577428131b07d9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 5 Nov 2022 13:18:54 +0100 Subject: [PATCH 235/547] [ADD]pms_api_rest: invoicing services flow --- pms_api_rest/datamodels/pms_mail.py | 1 + pms_api_rest/services/pms_invoice_service.py | 80 ++++++++++++++++--- .../pms_room_closure_reason_service.py | 2 +- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/pms_api_rest/datamodels/pms_mail.py b/pms_api_rest/datamodels/pms_mail.py index 211335834f..d91094f165 100644 --- a/pms_api_rest/datamodels/pms_mail.py +++ b/pms_api_rest/datamodels/pms_mail.py @@ -9,3 +9,4 @@ class PmsMailInfo(Datamodel): bodyMail = fields.String(required=False, allow_none=True) partnerIds = fields.List(fields.Integer(), required=False) emailAddresses = fields.List(fields.String(), required=False) + pmsPropertyCc = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 4412f313da..9494659fe0 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -59,23 +59,49 @@ def update_invoice(self, invoice_id, pms_invoice_info): line_values["name"] = line_info.name if line_info.quantity and line_info.quantity != line.quantity: line_values["quantity"] = line_info.quantity - new_vals["invoice_line_ids"].append((1, line.id, line_values)) + if line_values: + new_vals["invoice_line_ids"].append((1, line.id, line_values)) else: new_vals["invoice_line_ids"].append((2, line.id)) - for line_info in pms_invoice_info.moveLines: - if not line_info.id: - new_vals["invoice_line_ids"].append( - ( - 0, - 0, - { - "name": line_info.name, - "quantity": line_info.quantity, - "sale_line_ids": [(6, 0, line_info.saleLineId)], + # Get the new lines to add in invoice + new_invoice_lines_info = list( + filter(lambda item: not item.id, pms_invoice_info.moveLines) + ) + if new_invoice_lines_info: + 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, + ) + ) + } + ) + ) + new_vals["invoice_line_ids"].extend( + [ + item["invoice_line_ids"] + for item in folios.get_invoice_vals_list( + lines_to_invoice={ + new_invoice_lines_info[i] + .saleLineId: new_invoice_lines_info[i] + .quantity + for i in range(0, len(new_invoice_lines_info)) }, + partner_invoice_id=partner.id, ) - ) - + ][0] + ) if not new_vals: return invoice.id @@ -105,6 +131,13 @@ def update_invoice(self, invoice_id, pms_invoice_info): invoice = reverse_invoice invoice = self._direct_move_update(invoice, new_vals) + # Update invoice lines name + for item in pms_invoice_info.moveLines: + if item.saleLineId in invoice.invoice_line_ids.mapped("folio_line_ids.id"): + invoice_line = invoice.invoice_line_ids.filtered( + lambda r: item.saleLineId in r.folio_line_ids.ids + ) + invoice_line.write({"name": item.name}) return invoice.id def _direct_move_update(self, invoice, new_vals): @@ -130,4 +163,25 @@ def _direct_move_update(self, invoice, new_vals): 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) return True diff --git a/pms_api_rest/services/pms_room_closure_reason_service.py b/pms_api_rest/services/pms_room_closure_reason_service.py index bbe3aa10c3..0245acb690 100644 --- a/pms_api_rest/services/pms_room_closure_reason_service.py +++ b/pms_api_rest/services/pms_room_closure_reason_service.py @@ -27,7 +27,7 @@ def get_closure_reasons(self): for cl in self.env["room.closure.reason"].search([]): closure_reasons.append( PmsRoomClosureReasonInfo( - id=cl.id, name=cl.name, description=cl.description + id=cl.id, name=cl.name, description=cl.description or None ) ) return closure_reasons From 70e24b400653059b12daa9ccd7e24b86470cc068 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 1 Nov 2022 10:47:20 +0100 Subject: [PATCH 236/547] [IMP] pms_api_rest: add account payment service with some filters --- .../datamodels/pms_account_payment.py | 37 +++++++ pms_api_rest/services/__init__.py | 1 + .../services/pms_account_payment_service.py | 101 ++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 pms_api_rest/services/pms_account_payment_service.py diff --git a/pms_api_rest/datamodels/pms_account_payment.py b/pms_api_rest/datamodels/pms_account_payment.py index fbf96adcbc..490cd8c6a6 100644 --- a/pms_api_rest/datamodels/pms_account_payment.py +++ b/pms_api_rest/datamodels/pms_account_payment.py @@ -1,15 +1,52 @@ from marshmallow import fields from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel class PmsPaymentInfo(Datamodel): _name = "pms.payment.info" id = fields.Integer(required=False, allow_none=True) + name = fields.String(required=False, allow_none=True) amount = fields.Float(required=False, allow_none=True) journalId = fields.Integer(required=False, allow_none=True) date = fields.String(required=False, allow_none=True) + partnerName = fields.String(required=False, allow_none=True) + partnerId = fields.Integer(required=False, allow_none=True) paymentType = fields.String(required=False, allow_none=True) + partnerType = fields.String(required=False, allow_none=True) + isTransfer = fields.Boolean(required=False, allow_none=True) reference = fields.String(required=False, allow_none=True) + createUid = fields.Integer(required=False, allow_none=True) + + +class PmsPaymentSearchParam(Datamodel): + _name = "pms.payment.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) + paymentMethodId = fields.Integer(required=False, allow_none=True) + # TODO: paymentTypes filter + paymentTypes = fields.List(fields.Integer(required=False, allow_none=True)) + paymentType = fields.String(required=False, allow_none=True) + partnerType = fields.String(required=False, allow_none=True) + isTransfer = fields.Boolean(required=False, allow_none=True) + + +class PmsPaymentResults(Datamodel): + _name = "pms.payment.results" + payments = fields.List(NestedModel("pms.payment.info")) + total = fields.Integer(required=False, allow_none=True) + totalPayments = fields.Integer(required=False, allow_none=True) + + +class PmsAccountPaymentInfo(Datamodel): + _name = "pms.account.payment.short.info" + id = fields.Integer(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) diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index f38e8f73d8..371cd165c9 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -35,6 +35,7 @@ from . import pms_room_closure_reason_service from . import res_lang_service +from . import pms_account_payment_service from . import pms_account_payment_terms_service from . import pms_account_journal_service from . import pms_invoice_service diff --git a/pms_api_rest/services/pms_account_payment_service.py b/pms_api_rest/services/pms_account_payment_service.py new file mode 100644 index 0000000000..b23c8136d1 --- /dev/null +++ b/pms_api_rest/services/pms_account_payment_service.py @@ -0,0 +1,101 @@ +from datetime import datetime +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.odoo import fields +from odoo.osv import expression + + +class PmsAccountPaymentService(Component): + _inherit = "base.rest.service" + _name = "pms.account.payment.service" + _usage = "payments" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.payment.search.param", is_list=False), + output_param=Datamodel("pms.payment.results", is_list=False), + auth="jwt_api_pms", + ) + def get_payments(self, pms_payments_search_param): + result_payments = [] + domain_fields = [] + available_journals = () + if pms_payments_search_param.pmsPropertyId: + available_journals = self.env["account.journal"].search( + [ + "&", + ("pms_property_ids", "in", pms_payments_search_param.pmsPropertyId), + ("pms_property_ids", "!=", False), + ] + ) + domain_fields.append(("journal_id", "in", available_journals.ids)) + domain_filter=list() + if pms_payments_search_param.filter: + # TODO: filter by folio and invoice + for search in pms_payments_search_param.filter.split(" "): + subdomains = [ + [("name", "ilike", search)], + # [("folio_id.name", "ilike", search)], + [("partner_id.display_name", "ilike", search)], + ] + domain_filter.append(expression.OR(subdomains)) + + if pms_payments_search_param.dateStart and pms_payments_search_param.dateEnd: + date_from = fields.Date.from_string(pms_payments_search_param.dateStart) + date_to = fields.Date.from_string(pms_payments_search_param.dateEnd) + domain_fields.extend([ + "&", + ("date", ">=", date_from), + ("date", "<", date_to), + ]) + if pms_payments_search_param.paymentMethodId: + domain_fields.append(("journal_id","=",pms_payments_search_param.paymentMethodId)) + # TODO: payment tyope filter (partner_type, payment_type, is_transfer) + if domain_filter: + domain = expression.AND([domain_fields, domain_filter[0]]) + else: + domain = domain_fields + + PmsPaymentResults = self.env.datamodels["pms.payment.results"] + PmsPaymentInfo = self.env.datamodels["pms.payment.info"] + + total_payments = self.env["account.payment"].search_count(domain) + for payment in self.env["account.payment"].search( + domain, + order=pms_payments_search_param.orderBy, + limit=pms_payments_search_param.limit, + offset=pms_payments_search_param.offset, + ): + result_payments.append( + PmsPaymentInfo( + 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, + date=payment.date.strftime("%d/%m/%Y"), + partnerId = payment.partner_id.id + if payment.partner_id + else None, + partnerName = payment.partner_id.name + if payment.partner_id + else None, + paymentType=payment.payment_type, + partnerType=payment.partner_type, + isTransfer=payment.is_internal_transfer, + reference=payment.ref if payment.ref else None, + createUid=payment.create_uid if payment.create_uid else None, + ) + ) + + return PmsPaymentResults(payments=result_payments, total=23333, totalPayments=total_payments) From 0c06e3b4e3f93aa9b8041fbd8f3130fee3b3aa0f Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 1 Nov 2022 11:46:15 +0100 Subject: [PATCH 237/547] [IMP] pms_api_rest: add total in payment service --- .../services/pms_account_payment_service.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_account_payment_service.py b/pms_api_rest/services/pms_account_payment_service.py index b23c8136d1..7f83468282 100644 --- a/pms_api_rest/services/pms_account_payment_service.py +++ b/pms_api_rest/services/pms_account_payment_service.py @@ -27,7 +27,7 @@ class PmsAccountPaymentService(Component): ) def get_payments(self, pms_payments_search_param): result_payments = [] - domain_fields = [] + domain_fields = [("state","=","posted")] available_journals = () if pms_payments_search_param.pmsPropertyId: available_journals = self.env["account.journal"].search( @@ -69,6 +69,17 @@ def get_payments(self, pms_payments_search_param): PmsPaymentInfo = self.env.datamodels["pms.payment.info"] total_payments = self.env["account.payment"].search_count(domain) + group_payments = self.env["account.payment"].read_group( + domain=domain, + fields=["amount:sum"], + groupby=["payment_type"] + ) + amount_result = 0 + if group_payments: + for item in group_payments: + total_inbound = item["amount"] if item["payment_type"] == "inbound" else 0 + total_outbound = item["amount"] if item["payment_type"] == "outbound" else 0 + amount_result = total_inbound - total_outbound for payment in self.env["account.payment"].search( domain, order=pms_payments_search_param.orderBy, @@ -98,4 +109,4 @@ def get_payments(self, pms_payments_search_param): ) ) - return PmsPaymentResults(payments=result_payments, total=23333, totalPayments=total_payments) + return PmsPaymentResults(payments=result_payments, total=amount_result, totalPayments=total_payments) From faa4a5590f7597e1fe15b2afeb6962b75b84c3d1 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 2 Nov 2022 11:43:50 +0100 Subject: [PATCH 238/547] [IMP] pms_api_rest: add cash register service --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_cash_register.py | 17 ++++ .../services/pms_account_payment_service.py | 92 +++++++++++++------ 3 files changed, 84 insertions(+), 26 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_cash_register.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 186d4d7bfa..12813d93f6 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -52,6 +52,7 @@ from . import pms_room_closure_reason from . import pms_report +from . import pms_cash_register from . import pms_folio_sale_line from . import pms_invoice_line from . import pms_mail 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..bc18328e09 --- /dev/null +++ b/pms_api_rest/datamodels/pms_cash_register.py @@ -0,0 +1,17 @@ +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.Integer(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) diff --git a/pms_api_rest/services/pms_account_payment_service.py b/pms_api_rest/services/pms_account_payment_service.py index 7f83468282..ee9bcc4956 100644 --- a/pms_api_rest/services/pms_account_payment_service.py +++ b/pms_api_rest/services/pms_account_payment_service.py @@ -1,9 +1,9 @@ -from datetime import datetime +from odoo.odoo import fields +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 -from odoo.odoo import fields -from odoo.osv import expression class PmsAccountPaymentService(Component): @@ -27,7 +27,7 @@ class PmsAccountPaymentService(Component): ) def get_payments(self, pms_payments_search_param): result_payments = [] - domain_fields = [("state","=","posted")] + domain_fields = [("state", "=", "posted")] available_journals = () if pms_payments_search_param.pmsPropertyId: available_journals = self.env["account.journal"].search( @@ -38,7 +38,7 @@ def get_payments(self, pms_payments_search_param): ] ) domain_fields.append(("journal_id", "in", available_journals.ids)) - domain_filter=list() + domain_filter = list() if pms_payments_search_param.filter: # TODO: filter by folio and invoice for search in pms_payments_search_param.filter.split(" "): @@ -52,13 +52,17 @@ def get_payments(self, pms_payments_search_param): if pms_payments_search_param.dateStart and pms_payments_search_param.dateEnd: date_from = fields.Date.from_string(pms_payments_search_param.dateStart) date_to = fields.Date.from_string(pms_payments_search_param.dateEnd) - domain_fields.extend([ - "&", - ("date", ">=", date_from), - ("date", "<", date_to), - ]) + domain_fields.extend( + [ + "&", + ("date", ">=", date_from), + ("date", "<", date_to), + ] + ) if pms_payments_search_param.paymentMethodId: - domain_fields.append(("journal_id","=",pms_payments_search_param.paymentMethodId)) + domain_fields.append( + ("journal_id", "=", pms_payments_search_param.paymentMethodId) + ) # TODO: payment tyope filter (partner_type, payment_type, is_transfer) if domain_filter: domain = expression.AND([domain_fields, domain_filter[0]]) @@ -70,15 +74,17 @@ def get_payments(self, pms_payments_search_param): total_payments = self.env["account.payment"].search_count(domain) group_payments = self.env["account.payment"].read_group( - domain=domain, - fields=["amount:sum"], - groupby=["payment_type"] + domain=domain, fields=["amount:sum"], groupby=["payment_type"] ) amount_result = 0 if group_payments: for item in group_payments: - total_inbound = item["amount"] if item["payment_type"] == "inbound" else 0 - total_outbound = item["amount"] if item["payment_type"] == "outbound" else 0 + total_inbound = ( + item["amount"] if item["payment_type"] == "inbound" else 0 + ) + total_outbound = ( + item["amount"] if item["payment_type"] == "outbound" else 0 + ) amount_result = total_inbound - total_outbound for payment in self.env["account.payment"].search( domain, @@ -91,16 +97,10 @@ def get_payments(self, pms_payments_search_param): 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, + journalId=payment.journal_id.id if payment.journal_id else None, date=payment.date.strftime("%d/%m/%Y"), - partnerId = payment.partner_id.id - if payment.partner_id - else None, - partnerName = payment.partner_id.name - if payment.partner_id - else None, + partnerId=payment.partner_id.id if payment.partner_id else None, + partnerName=payment.partner_id.name if payment.partner_id else None, paymentType=payment.payment_type, partnerType=payment.partner_type, isTransfer=payment.is_internal_transfer, @@ -109,4 +109,44 @@ def get_payments(self, pms_payments_search_param): ) ) - return PmsPaymentResults(payments=result_payments, total=amount_result, totalPayments=total_payments) + return PmsPaymentResults( + payments=result_payments, total=amount_result, totalPayments=total_payments + ) + + @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.env["account.bank.statement"] + .sudo() + .search( + [ + ("journal_id", "=", cash_register_search_param.journalId), + ], + limit=1, + ) + ) + + CashRegister = self.env.datamodels["pms.cash.register.info"] + if not statement: + return CashRegister() + isOpen = True if statement.state == "open" else False + 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=statement.create_date.strftime("%d/%m/%Y") + if isOpen + else statement.date_done.strftime("%d/%m/%Y"), + ) From f3aa28af766dcf509a66bffe33bbae8214781a62 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 2 Nov 2022 14:52:16 +0100 Subject: [PATCH 239/547] [IMP] pms_api_rest: add open and close cash methods --- .../datamodels/pms_account_journal.py | 1 + .../datamodels/pms_account_payment.py | 2 +- pms_api_rest/datamodels/pms_cash_register.py | 17 ++- .../services/pms_account_journal_service.py | 1 + .../services/pms_account_payment_service.py | 117 ++++++++++++++++++ 5 files changed, 136 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_account_journal.py b/pms_api_rest/datamodels/pms_account_journal.py index 76a076b048..6b9f4c5128 100644 --- a/pms_api_rest/datamodels/pms_account_journal.py +++ b/pms_api_rest/datamodels/pms_account_journal.py @@ -12,4 +12,5 @@ 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.py b/pms_api_rest/datamodels/pms_account_payment.py index 490cd8c6a6..0b27e9c4ad 100644 --- a/pms_api_rest/datamodels/pms_account_payment.py +++ b/pms_api_rest/datamodels/pms_account_payment.py @@ -38,7 +38,7 @@ class PmsPaymentSearchParam(Datamodel): class PmsPaymentResults(Datamodel): _name = "pms.payment.results" payments = fields.List(NestedModel("pms.payment.info")) - total = fields.Integer(required=False, allow_none=True) + total = fields.Float(required=False, allow_none=True) totalPayments = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_cash_register.py b/pms_api_rest/datamodels/pms_cash_register.py index bc18328e09..672639e027 100644 --- a/pms_api_rest/datamodels/pms_cash_register.py +++ b/pms_api_rest/datamodels/pms_cash_register.py @@ -8,10 +8,25 @@ class PmsCashRegisterInfo(Datamodel): 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.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/services/pms_account_journal_service.py b/pms_api_rest/services/pms_account_journal_service.py index 69d211d299..4ce6d9aaeb 100644 --- a/pms_api_rest/services/pms_account_journal_service.py +++ b/pms_api_rest/services/pms_account_journal_service.py @@ -39,6 +39,7 @@ def get_method_payments(self, account_journal_search_param): PmsAccountJournalInfo( id=payment_method.id, name=payment_method.name, + type=payment_method.type, allowedPayments=payment_method.allowed_pms_payments, ) ) diff --git a/pms_api_rest/services/pms_account_payment_service.py b/pms_api_rest/services/pms_account_payment_service.py index ee9bcc4956..a42a448422 100644 --- a/pms_api_rest/services/pms_account_payment_service.py +++ b/pms_api_rest/services/pms_account_payment_service.py @@ -1,4 +1,9 @@ +from datetime import datetime + +from odoo import _ from odoo.odoo import fields +from odoo.odoo.exceptions import ValidationError +from odoo.odoo.tools import get_lang from odoo.osv import expression from odoo.addons.base_rest import restapi @@ -150,3 +155,115 @@ def get_cash_register(self, cash_register_search_param): if isOpen else statement.date_done.strftime("%d/%m/%Y"), ) + + @restapi.method( + [ + ( + [ + "/p/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.env["account.bank.statement"] + .sudo() + .search( + [ + ("journal_id", "=", journal_id), + ], + limit=1, + ) + ) + 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.env["account.bank.statement"] + .sudo() + .search( + [ + ("journal_id", "=", journal_id), + ("state", "=", "open"), + ("pms_property_id", "=", pms_property_id), + ], + limit=1, + ) + ) + if round(statement.balance_end, 2) == round(amount, 2): + statement.sudo().balance_end_real = amount + statement.sudo().button_post() + return { + "result": True, + "diff": 0, + } + elif force: + # 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) + diff = round(amount - statement.balance_end, 2) + return { + "result": True, + "diff": diff, + } + else: + diff = round(amount - statement.balance_end, 2) + return { + "result": False, + "diff": diff, + } From f18ae50f5524ff28f09200e0c2481450afb084d3 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 2 Nov 2022 20:16:19 +0100 Subject: [PATCH 240/547] [IMP] pms_api_rest: add image in users datamodel --- pms_api_rest/datamodels/res_users.py | 2 ++ pms_api_rest/services/pms_property_service.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/res_users.py b/pms_api_rest/datamodels/res_users.py index 8ba270427c..392a8b6cc4 100644 --- a/pms_api_rest/datamodels/res_users.py +++ b/pms_api_rest/datamodels/res_users.py @@ -7,3 +7,5 @@ class PmsResUsersInfo(Datamodel): _name = "res.users.info" id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) + userImageBase64 = fields.String(required=False, allow_none=True) + diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 8b00af54d7..85f491be38 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -107,5 +107,9 @@ def get_users(self, pms_property_id): [("pms_property_ids", "in", pms_property_id)] ) for user in users: - result_users.append(ResUsersInfo(id=user.id, name=user.name)) + result_users.append(ResUsersInfo( + id=user.id, + name=user.name, + userImageBase64=user.partner_id.image_1024 or None + )) return result_users From 0eda5d619e6bc64b3430ebe14894009683f8eed8 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 3 Nov 2022 12:58:01 +0100 Subject: [PATCH 241/547] [IMP] pms_api_rest: add payment report service --- pms_api_rest/datamodels/__init__.py | 1 + .../datamodels/pms_payment_report_input.py | 17 ++++++++++ pms_api_rest/datamodels/res_users.py | 1 - .../services/pms_account_payment_service.py | 34 +++++++++++++++++++ pms_api_rest/services/pms_property_service.py | 12 ++++--- 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_payment_report_input.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 12813d93f6..25586a00a1 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -53,6 +53,7 @@ from . import pms_room_closure_reason from . import pms_report from . import pms_cash_register +from . import pms_payment_report_input from . import pms_folio_sale_line from . import pms_invoice_line from . import pms_mail diff --git a/pms_api_rest/datamodels/pms_payment_report_input.py b/pms_api_rest/datamodels/pms_payment_report_input.py new file mode 100644 index 0000000000..b100556f17 --- /dev/null +++ b/pms_api_rest/datamodels/pms_payment_report_input.py @@ -0,0 +1,17 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel +from odoo.addons.datamodel.fields import NestedModel + + +class PmsPaymentReportSearchParam(Datamodel): + _name = "pms.payment.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 PmsPaymentReportInput(Datamodel): + _name = "pms.payment.report" + fileName = fields.String(required=False, allow_none=True) + binary = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/res_users.py b/pms_api_rest/datamodels/res_users.py index 392a8b6cc4..f003f46eb4 100644 --- a/pms_api_rest/datamodels/res_users.py +++ b/pms_api_rest/datamodels/res_users.py @@ -8,4 +8,3 @@ class PmsResUsersInfo(Datamodel): id = fields.Integer(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) userImageBase64 = fields.String(required=False, allow_none=True) - diff --git a/pms_api_rest/services/pms_account_payment_service.py b/pms_api_rest/services/pms_account_payment_service.py index a42a448422..35b51c956d 100644 --- a/pms_api_rest/services/pms_account_payment_service.py +++ b/pms_api_rest/services/pms_account_payment_service.py @@ -267,3 +267,37 @@ def _action_close_cash_session(self, pms_property_id, amount, journal_id, force) "result": False, "diff": diff, } + + @restapi.method( + [ + ( + [ + "/payment-report", + ], + "GET", + ) + ], + input_param=Datamodel("pms.payment.report.search.param", is_list=False), + output_param=Datamodel("pms.payment.report", is_list=False), + auth="jwt_api_pms", + ) + def payment_report(self, pms_payment_report_search_param): + pms_property_id = pms_payment_report_search_param.pmsPropertyId + date_from = pms_payment_report_search_param.dateFrom + date_to = pms_payment_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.payment.report"] + # REVIEW: Reuse pms.report.info by modifying the fields + # to support different types of documents? + # proposal: contentBase64 = fields.String, + # fileType = fields.String (pdf, xlsx, etc...) + return PmsResponse(fileName=file_name, binary=base64EncodedStr) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 85f491be38..6818860fa3 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -107,9 +107,11 @@ def get_users(self, pms_property_id): [("pms_property_ids", "in", pms_property_id)] ) for user in users: - result_users.append(ResUsersInfo( - id=user.id, - name=user.name, - userImageBase64=user.partner_id.image_1024 or None - )) + result_users.append( + ResUsersInfo( + id=user.id, + name=user.name, + userImageBase64=user.partner_id.image_1024 or None, + ) + ) return result_users From f68c2c97cd2d33e49187c8362212af24bc602420 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 4 Nov 2022 11:11:49 +0100 Subject: [PATCH 242/547] [WIP] pms_api_rest: refact payment info datamodel --- pms_api_rest/datamodels/pms_account_payment.py | 11 +++++++++-- pms_api_rest/datamodels/pms_payment_report_input.py | 1 - pms_api_rest/services/pms_account_payment_service.py | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/datamodels/pms_account_payment.py b/pms_api_rest/datamodels/pms_account_payment.py index 0b27e9c4ad..190ac83694 100644 --- a/pms_api_rest/datamodels/pms_account_payment.py +++ b/pms_api_rest/datamodels/pms_account_payment.py @@ -42,11 +42,18 @@ class PmsPaymentResults(Datamodel): totalPayments = fields.Integer(required=False, allow_none=True) -class PmsAccountPaymentInfo(Datamodel): - _name = "pms.account.payment.short.info" +class PmsTransactionInfo(Datamodel): + _name = "pms.transaction.info" id = fields.Integer(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) + + transactionType = fields.String(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) diff --git a/pms_api_rest/datamodels/pms_payment_report_input.py b/pms_api_rest/datamodels/pms_payment_report_input.py index b100556f17..ccc5c09282 100644 --- a/pms_api_rest/datamodels/pms_payment_report_input.py +++ b/pms_api_rest/datamodels/pms_payment_report_input.py @@ -1,7 +1,6 @@ from marshmallow import fields from odoo.addons.datamodel.core import Datamodel -from odoo.addons.datamodel.fields import NestedModel class PmsPaymentReportSearchParam(Datamodel): diff --git a/pms_api_rest/services/pms_account_payment_service.py b/pms_api_rest/services/pms_account_payment_service.py index 35b51c956d..43f7051d38 100644 --- a/pms_api_rest/services/pms_account_payment_service.py +++ b/pms_api_rest/services/pms_account_payment_service.py @@ -153,7 +153,9 @@ def get_cash_register(self, cash_register_search_param): balance=statement.balance_start if isOpen else statement.balance_end_real, dateTime=statement.create_date.strftime("%d/%m/%Y") if isOpen - else statement.date_done.strftime("%d/%m/%Y"), + else statement.date_done.strftime("%d/%m/%Y") + if statement.date_done + else None, ) @restapi.method( From 9d26d6ccf00fbe7a8f76f310cf5e38af07e2422e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 4 Nov 2022 13:38:44 +0100 Subject: [PATCH 243/547] [RFC]pms_api_rest: payments to transactions --- pms_api_rest/datamodels/__init__.py | 2 +- .../datamodels/pms_account_payment.py | 59 ------ pms_api_rest/datamodels/pms_transaction.py | 41 +++++ pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/account_payment.py | 36 ++++ pms_api_rest/services/pms_folio_service.py | 12 +- pms_api_rest/services/pms_partner_service.py | 7 +- ..._service.py => pms_transaction_service.py} | 170 +++++++++++++----- 8 files changed, 213 insertions(+), 115 deletions(-) delete mode 100644 pms_api_rest/datamodels/pms_account_payment.py create mode 100644 pms_api_rest/datamodels/pms_transaction.py create mode 100644 pms_api_rest/models/account_payment.py rename pms_api_rest/services/{pms_account_payment_service.py => pms_transaction_service.py} (64%) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 25586a00a1..bba5766fd3 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -14,7 +14,7 @@ from . import pms_property from . import pms_account_journal -from . import pms_account_payment +from . import pms_transaction from . import pms_invoice from . import pms_user diff --git a/pms_api_rest/datamodels/pms_account_payment.py b/pms_api_rest/datamodels/pms_account_payment.py deleted file mode 100644 index 190ac83694..0000000000 --- a/pms_api_rest/datamodels/pms_account_payment.py +++ /dev/null @@ -1,59 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel -from odoo.addons.datamodel.fields import NestedModel - - -class PmsPaymentInfo(Datamodel): - _name = "pms.payment.info" - id = fields.Integer(required=False, allow_none=True) - name = fields.String(required=False, allow_none=True) - amount = fields.Float(required=False, allow_none=True) - journalId = fields.Integer(required=False, allow_none=True) - date = fields.String(required=False, allow_none=True) - partnerName = fields.String(required=False, allow_none=True) - partnerId = fields.Integer(required=False, allow_none=True) - paymentType = fields.String(required=False, allow_none=True) - partnerType = fields.String(required=False, allow_none=True) - isTransfer = fields.Boolean(required=False, allow_none=True) - reference = fields.String(required=False, allow_none=True) - createUid = fields.Integer(required=False, allow_none=True) - - -class PmsPaymentSearchParam(Datamodel): - _name = "pms.payment.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) - paymentMethodId = fields.Integer(required=False, allow_none=True) - # TODO: paymentTypes filter - paymentTypes = fields.List(fields.Integer(required=False, allow_none=True)) - paymentType = fields.String(required=False, allow_none=True) - partnerType = fields.String(required=False, allow_none=True) - isTransfer = fields.Boolean(required=False, allow_none=True) - - -class PmsPaymentResults(Datamodel): - _name = "pms.payment.results" - payments = fields.List(NestedModel("pms.payment.info")) - total = fields.Float(required=False, allow_none=True) - totalPayments = fields.Integer(required=False, allow_none=True) - - -class PmsTransactionInfo(Datamodel): - _name = "pms.transaction.info" - id = fields.Integer(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) - - transactionType = fields.String(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) diff --git a/pms_api_rest/datamodels/pms_transaction.py b/pms_api_rest/datamodels/pms_transaction.py new file mode 100644 index 0000000000..28465f816a --- /dev/null +++ b/pms_api_rest/datamodels/pms_transaction.py @@ -0,0 +1,41 @@ +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) + # REVIEW: Fields to avoid?: + + +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) + 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) + # REVIEW: Fields to avoid?: + partnerName = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 49652c7020..495168b213 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -1,2 +1,3 @@ from . import pms_property from . import res_users +from . import account_payment diff --git a/pms_api_rest/models/account_payment.py b/pms_api_rest/models/account_payment.py new file mode 100644 index 0000000000..f6c3be67ab --- /dev/null +++ b/pms_api_rest/models/account_payment.py @@ -0,0 +1,36 @@ +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", + store=True, + ) + + @api.depends("payment_type", "partner_type") + def _compute_pms_api_transaction_type(self): + for record in self: + if record.is_internal_transfer: + return "internal_transfer" + if record.partner_type == "customer": + if record.payment_type == "inbound": + return "customer_payment" + else: + return "customer_refund" + if record.partner_type == "supplier": + if record.payment_type == "inbound": + return "supplier_payment" + else: + return "supplier_refund" + return False diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 1dc4095210..69403a867e 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -173,7 +173,7 @@ def get_folios(self, folio_search_param): ) ], input_param=Datamodel("pms.search.param"), - output_param=Datamodel("pms.payment.info", is_list=True), + output_param=Datamodel("pms.transaction.info", is_list=True), auth="jwt_api_pms", ) def get_folio_payments(self, folio_id, pms_search_param): @@ -183,7 +183,7 @@ def get_folio_payments(self, folio_id, pms_search_param): domain.append(("pms_property_id", "=", pms_search_param.pmsPropertyId)) folio = self.env["pms.folio"].search(domain) payments = [] - PmsPaymentInfo = self.env.datamodels["pms.payment.info"] + PmsTransactiontInfo = self.env.datamodels["pms.transaction.info"] if not folio: pass else: @@ -193,14 +193,14 @@ def get_folio_payments(self, folio_id, pms_search_param): if folio.payment_ids: for payment in folio.payment_ids: payments.append( - PmsPaymentInfo( + PmsTransactiontInfo( id=payment.id, amount=round(payment.amount, 2), journalId=payment.journal_id.id, date=datetime.combine( payment.date, datetime.min.time() ).isoformat(), - paymentType=payment.payment_type, + transactionTypeCode=payment.pms_api_transaction_type, ) ) return payments @@ -214,7 +214,7 @@ def get_folio_payments(self, folio_id, pms_search_param): "POST", ) ], - input_param=Datamodel("pms.payment.info", is_list=False), + input_param=Datamodel("pms.transaction.info", is_list=False), auth="jwt_api_pms", ) def create_folio_charge(self, folio_id, pms_account_payment_info): @@ -249,7 +249,7 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): "POST", ) ], - input_param=Datamodel("pms.payment.info", is_list=False), + input_param=Datamodel("pms.transaction.info", is_list=False), auth="jwt_api_pms", ) def create_folio_refund(self, folio_id, pms_account_payment_info): diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 08cff7a6a2..44d0c16036 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -216,23 +216,24 @@ def write_partner(self, partner_id, partner_info): "GET", ) ], - output_param=Datamodel("pms.payment.info", is_list=True), + 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)] ) - PmsPaymentInfo = self.env.datamodels["pms.payment.info"] + PmsTransactiontInfo = self.env.datamodels["pms.transaction.info"] payments = [] for payment in partnerPayments: payments.append( - PmsPaymentInfo( + PmsTransactiontInfo( id=payment.id, amount=round(payment.amount, 2), journalId=payment.journal_id.id, date=payment.date.strftime("%d/%m/%Y"), reference=payment.ref, + transactionTypeCode=payment.pms_api_transaction_type, ) ) return payments diff --git a/pms_api_rest/services/pms_account_payment_service.py b/pms_api_rest/services/pms_transaction_service.py similarity index 64% rename from pms_api_rest/services/pms_account_payment_service.py rename to pms_api_rest/services/pms_transaction_service.py index 43f7051d38..2d4cd298c7 100644 --- a/pms_api_rest/services/pms_account_payment_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -11,10 +11,10 @@ from odoo.addons.component.core import Component -class PmsAccountPaymentService(Component): +class PmsTransactionService(Component): _inherit = "base.rest.service" - _name = "pms.account.payment.service" - _usage = "payments" + _name = "pms.transaction.service" + _usage = "transactions" _collection = "pms.services" @restapi.method( @@ -26,37 +26,42 @@ class PmsAccountPaymentService(Component): "GET", ) ], - input_param=Datamodel("pms.payment.search.param", is_list=False), - output_param=Datamodel("pms.payment.results", is_list=False), + 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_payments(self, pms_payments_search_param): - result_payments = [] + def get_transactions(self, pms_transactions_search_param): + result_transactions = [] domain_fields = [("state", "=", "posted")] available_journals = () - if pms_payments_search_param.pmsPropertyId: + if pms_transactions_search_param.pmsPropertyId: available_journals = self.env["account.journal"].search( [ - "&", - ("pms_property_ids", "in", pms_payments_search_param.pmsPropertyId), - ("pms_property_ids", "!=", False), + ( + "pms_property_ids", + "in", + pms_transactions_search_param.pmsPropertyId, + ), ] ) domain_fields.append(("journal_id", "in", available_journals.ids)) domain_filter = list() - if pms_payments_search_param.filter: + if pms_transactions_search_param.filter: # TODO: filter by folio and invoice - for search in pms_payments_search_param.filter.split(" "): + for search in pms_transactions_search_param.filter.split(" "): subdomains = [ [("name", "ilike", search)], - # [("folio_id.name", "ilike", search)], + [("ref", "ilike", search)], [("partner_id.display_name", "ilike", search)], ] domain_filter.append(expression.OR(subdomains)) - if pms_payments_search_param.dateStart and pms_payments_search_param.dateEnd: - date_from = fields.Date.from_string(pms_payments_search_param.dateStart) - date_to = fields.Date.from_string(pms_payments_search_param.dateEnd) + 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( [ "&", @@ -64,26 +69,33 @@ def get_payments(self, pms_payments_search_param): ("date", "<", date_to), ] ) - if pms_payments_search_param.paymentMethodId: + if pms_transactions_search_param.transactionMethodId: + domain_fields.append( + ("journal_id", "=", pms_transactions_search_param.transactionMethodId) + ) + + if pms_transactions_search_param.transactionType: domain_fields.append( - ("journal_id", "=", pms_payments_search_param.paymentMethodId) + "pms_api_transaction_type", + "=", + pms_transactions_search_param.transactionType, ) - # TODO: payment tyope filter (partner_type, payment_type, is_transfer) + if domain_filter: domain = expression.AND([domain_fields, domain_filter[0]]) else: domain = domain_fields - PmsPaymentResults = self.env.datamodels["pms.payment.results"] - PmsPaymentInfo = self.env.datamodels["pms.payment.info"] + PmsTransactionResults = self.env.datamodels["pms.transaction.results"] + PmsTransactiontInfo = self.env.datamodels["pms.transaction.info"] - total_payments = self.env["account.payment"].search_count(domain) - group_payments = self.env["account.payment"].read_group( + 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_payments: - for item in group_payments: + if group_transactions: + for item in group_transactions: total_inbound = ( item["amount"] if item["payment_type"] == "inbound" else 0 ) @@ -91,33 +103,99 @@ def get_payments(self, pms_payments_search_param): item["amount"] if item["payment_type"] == "outbound" else 0 ) amount_result = total_inbound - total_outbound - for payment in self.env["account.payment"].search( + for transaction in self.env["account.payment"].search( domain, - order=pms_payments_search_param.orderBy, - limit=pms_payments_search_param.limit, - offset=pms_payments_search_param.offset, + order=pms_transactions_search_param.orderBy, + limit=pms_transactions_search_param.limit, + offset=pms_transactions_search_param.offset, ): - result_payments.append( - PmsPaymentInfo( - 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, - date=payment.date.strftime("%d/%m/%Y"), - partnerId=payment.partner_id.id if payment.partner_id else None, - partnerName=payment.partner_id.name if payment.partner_id else None, - paymentType=payment.payment_type, - partnerType=payment.partner_type, - isTransfer=payment.is_internal_transfer, - reference=payment.ref if payment.ref else None, - createUid=payment.create_uid if payment.create_uid else None, + result_transactions.append( + 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, + date=transaction.date.strftime("%d/%m/%Y"), + 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, ) ) - return PmsPaymentResults( - payments=result_payments, total=amount_result, totalPayments=total_payments + return PmsTransactionResults( + payments=result_transactions, + total=amount_result, + totalPayments=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) + 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, + date=transaction.date.strftime("%d/%m/%Y"), + 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, ) + @restapi.method( + [ + ( + [ + "/", + ], + "POST", + ) + ], + input_param=Datamodel("pms.transaction.info", is_list=False), + auth="jwt_api_pms", + ) + def create_transaction(self): + return True + + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.transaction.info", is_list=False), + auth="jwt_api_pms", + ) + def update_transaction(self, transaction_id): + return True + @restapi.method( [ ( From a7edef0a77018def2b277cd8dcea6ac621f44cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 5 Nov 2022 17:30:12 +0100 Subject: [PATCH 244/547] =?UTF-8?q?[RFC]pms=5Fapi=5Frest:=20imrpovement=20?= =?UTF-8?q?refact=20transactions=C3=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pms_api_rest/datamodels/__init__.py | 2 +- .../datamodels/pms_account_payment_term.py | 4 +- pms_api_rest/datamodels/pms_mail.py | 1 - pms_api_rest/datamodels/pms_transaction.py | 1 + ...ort_input.py => pms_transaction_report.py} | 8 +-- pms_api_rest/models/account_payment.py | 18 +++--- pms_api_rest/services/__init__.py | 2 +- .../pms_account_payment_terms_service.py | 6 +- pms_api_rest/services/pms_folio_service.py | 13 ++-- pms_api_rest/services/pms_partner_service.py | 2 +- .../services/pms_transaction_service.py | 60 +++++++++++-------- 11 files changed, 64 insertions(+), 53 deletions(-) rename pms_api_rest/datamodels/{pms_payment_report_input.py => pms_transaction_report.py} (68%) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index bba5766fd3..75bef6942c 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -53,7 +53,7 @@ from . import pms_room_closure_reason from . import pms_report from . import pms_cash_register -from . import pms_payment_report_input +from . import pms_transaction_report from . import pms_folio_sale_line from . import pms_invoice_line from . import pms_mail diff --git a/pms_api_rest/datamodels/pms_account_payment_term.py b/pms_api_rest/datamodels/pms_account_payment_term.py index 2bfd8d53de..74e15fb1b9 100644 --- a/pms_api_rest/datamodels/pms_account_payment_term.py +++ b/pms_api_rest/datamodels/pms_account_payment_term.py @@ -3,7 +3,7 @@ from odoo.addons.datamodel.core import Datamodel -class PmsAccountPaymentTermInfo(Datamodel): - _name = "pms.account.payment.term.info" +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_mail.py b/pms_api_rest/datamodels/pms_mail.py index d91094f165..211335834f 100644 --- a/pms_api_rest/datamodels/pms_mail.py +++ b/pms_api_rest/datamodels/pms_mail.py @@ -9,4 +9,3 @@ class PmsMailInfo(Datamodel): bodyMail = fields.String(required=False, allow_none=True) partnerIds = fields.List(fields.Integer(), required=False) emailAddresses = fields.List(fields.String(), required=False) - pmsPropertyCc = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_transaction.py b/pms_api_rest/datamodels/pms_transaction.py index 28465f816a..5295489336 100644 --- a/pms_api_rest/datamodels/pms_transaction.py +++ b/pms_api_rest/datamodels/pms_transaction.py @@ -26,6 +26,7 @@ class PmsTransactionsResults(Datamodel): 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) diff --git a/pms_api_rest/datamodels/pms_payment_report_input.py b/pms_api_rest/datamodels/pms_transaction_report.py similarity index 68% rename from pms_api_rest/datamodels/pms_payment_report_input.py rename to pms_api_rest/datamodels/pms_transaction_report.py index ccc5c09282..fb5d097fa8 100644 --- a/pms_api_rest/datamodels/pms_payment_report_input.py +++ b/pms_api_rest/datamodels/pms_transaction_report.py @@ -3,14 +3,14 @@ from odoo.addons.datamodel.core import Datamodel -class PmsPaymentReportSearchParam(Datamodel): - _name = "pms.payment.report.search.param" +class PmsTransactionReportSearchParam(Datamodel): + _name = "pms.transaction.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 PmsPaymentReportInput(Datamodel): - _name = "pms.payment.report" +class PmsTransactionReportInput(Datamodel): + _name = "pms.transaction.report" fileName = fields.String(required=False, allow_none=True) binary = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/models/account_payment.py b/pms_api_rest/models/account_payment.py index f6c3be67ab..5d5eb8750b 100644 --- a/pms_api_rest/models/account_payment.py +++ b/pms_api_rest/models/account_payment.py @@ -15,22 +15,22 @@ class AccountPayment(models.Model): string="PMS API Transaction Type", help="Transaction type for PMS API", compute="_compute_pms_api_transaction_type", - store=True, ) @api.depends("payment_type", "partner_type") def _compute_pms_api_transaction_type(self): for record in self: if record.is_internal_transfer: - return "internal_transfer" - if record.partner_type == "customer": + record.pms_api_transaction_type = "internal_transfer" + elif record.partner_type == "customer": if record.payment_type == "inbound": - return "customer_payment" + record.pms_api_transaction_type = "customer_inbound" else: - return "customer_refund" - if record.partner_type == "supplier": + record.pms_api_transaction_type = "customer_outbound" + elif record.partner_type == "supplier": if record.payment_type == "inbound": - return "supplier_payment" + record.pms_api_transaction_type = "supplier_outbound" else: - return "supplier_refund" - return False + record.pms_api_transaction_type = "supplier_inbound" + else: + record.pms_api_transaction_type = False diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index 371cd165c9..cab1415013 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -35,7 +35,7 @@ from . import pms_room_closure_reason_service from . import res_lang_service -from . import pms_account_payment_service +from . import pms_transaction_service from . import pms_account_payment_terms_service from . import pms_account_journal_service from . import pms_invoice_service diff --git a/pms_api_rest/services/pms_account_payment_terms_service.py b/pms_api_rest/services/pms_account_payment_terms_service.py index 14e4281087..1589d00142 100644 --- a/pms_api_rest/services/pms_account_payment_terms_service.py +++ b/pms_api_rest/services/pms_account_payment_terms_service.py @@ -18,12 +18,14 @@ class PmsAccountPaymentTermService(Component): "GET", ) ], - output_param=Datamodel("pms.account.payment.term.info", is_list=True), + 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.payment.term.info"] + PmsAccountPaymenttermInfo = self.env.datamodels[ + "pms.account.transaction.term.info" + ] res = [] for payment_term in self.env["account.payment.term"].search([]): res.append( diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 69403a867e..277116468b 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -167,7 +167,7 @@ def get_folios(self, folio_search_param): [ ( [ - "//payments", + "//transactions", ], "GET", ) @@ -176,13 +176,13 @@ def get_folios(self, folio_search_param): output_param=Datamodel("pms.transaction.info", is_list=True), auth="jwt_api_pms", ) - def get_folio_payments(self, folio_id, pms_search_param): + 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) - payments = [] + transactions = [] PmsTransactiontInfo = self.env.datamodels["pms.transaction.info"] if not folio: pass @@ -192,7 +192,8 @@ def get_folio_payments(self, folio_id, pms_search_param): # else: if folio.payment_ids: for payment in folio.payment_ids: - payments.append( + payment._compute_pms_api_transaction_type() + transactions.append( PmsTransactiontInfo( id=payment.id, amount=round(payment.amount, 2), @@ -200,10 +201,10 @@ def get_folio_payments(self, folio_id, pms_search_param): date=datetime.combine( payment.date, datetime.min.time() ).isoformat(), - transactionTypeCode=payment.pms_api_transaction_type, + transactionType=payment.pms_api_transaction_type, ) ) - return payments + return transactions @restapi.method( [ diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 44d0c16036..a8460f9b66 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -233,7 +233,7 @@ def get_partner_payments(self, partner_id): journalId=payment.journal_id.id, date=payment.date.strftime("%d/%m/%Y"), reference=payment.ref, - transactionTypeCode=payment.pms_api_transaction_type, + transactionType=payment.pms_api_transaction_type, ) ) return payments diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 2d4cd298c7..cd7ca46ccd 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -1,10 +1,9 @@ from datetime import datetime -from odoo import _ -from odoo.odoo import fields -from odoo.odoo.exceptions import ValidationError -from odoo.odoo.tools import get_lang +from odoo import _, fields +from odoo.exceptions import 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 @@ -95,13 +94,23 @@ def get_transactions(self, pms_transactions_search_param): ) amount_result = 0 if group_transactions: - for item in group_transactions: - total_inbound = ( - item["amount"] if item["payment_type"] == "inbound" else 0 - ) - total_outbound = ( - item["amount"] if item["payment_type"] == "outbound" else 0 - ) + 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 amount_result = total_inbound - total_outbound for transaction in self.env["account.payment"].search( domain, @@ -113,7 +122,7 @@ def get_transactions(self, pms_transactions_search_param): PmsTransactiontInfo( id=transaction.id, name=transaction.name if transaction.name else None, - amount=transaction.amount, + amount=round(transaction.amount, 2), journalId=transaction.journal_id.id if transaction.journal_id else None, @@ -128,14 +137,13 @@ def get_transactions(self, pms_transactions_search_param): createUid=transaction.create_uid if transaction.create_uid else None, - transactionType=transaction.pms_api_transaction_type, + transactionType=transaction.pms_api_transaction_type or None, ) ) - return PmsTransactionResults( - payments=result_transactions, - total=amount_result, - totalPayments=total_transactions, + transactions=result_transactions, + total=round(amount_result, 2), + totalTransactions=total_transactions, ) @restapi.method( @@ -163,7 +171,7 @@ def get_transaction(self, transaction_id): 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, + transactionType=transaction.pms_api_transaction_type or None, ) @restapi.method( @@ -352,19 +360,19 @@ def _action_close_cash_session(self, pms_property_id, amount, journal_id, force) [ ( [ - "/payment-report", + "/transactions-report", ], "GET", ) ], - input_param=Datamodel("pms.payment.report.search.param", is_list=False), - output_param=Datamodel("pms.payment.report", is_list=False), + input_param=Datamodel("pms.transaction.report.search.param", is_list=False), + output_param=Datamodel("pms.transaction.report", is_list=False), auth="jwt_api_pms", ) - def payment_report(self, pms_payment_report_search_param): - pms_property_id = pms_payment_report_search_param.pmsPropertyId - date_from = pms_payment_report_search_param.dateFrom - date_to = pms_payment_report_search_param.dateTo + def transactions_report(self, pms_transaction_report_search_param): + pms_property_id = pms_transaction_report_search_param.pmsPropertyId + date_from = pms_transaction_report_search_param.dateFrom + date_to = pms_transaction_report_search_param.dateTo report_wizard = self.env["cash.daily.report.wizard"].create( { "date_start": date_from, @@ -375,7 +383,7 @@ def payment_report(self, pms_payment_report_search_param): result = report_wizard._export() file_name = result["xls_filename"] base64EncodedStr = result["xls_binary"] - PmsResponse = self.env.datamodels["pms.payment.report"] + PmsResponse = self.env.datamodels["pms.transaction.report"] # REVIEW: Reuse pms.report.info by modifying the fields # to support different types of documents? # proposal: contentBase64 = fields.String, From 93b8d97e095227e32e7242d014ea91de0843d8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 6 Nov 2022 09:57:36 +0100 Subject: [PATCH 245/547] [ADD]pms_api_rest: invoicing proforma report --- pms_api_rest/services/pms_folio_service.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 277116468b..18b44038fd 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -527,9 +527,14 @@ def get_folio_invoices(self, folio_id): else None, ) ) + 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.get_portal_url() + + move_url ) invoices.append( PmsFolioInvoiceInfo( From e0ea0380cf2e2ff06ade9ef49b9d0af1a46bcbb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 6 Nov 2022 11:13:48 +0100 Subject: [PATCH 246/547] [RFC]pms_api_rest: user unique report datamodel --- pms_api_rest/datamodels/__init__.py | 3 +-- pms_api_rest/datamodels/pms_report.py | 14 +++++++++++--- .../datamodels/pms_transaction_report.py | 16 ---------------- pms_api_rest/services/pms_reservation_service.py | 12 ++++++------ pms_api_rest/services/pms_transaction_service.py | 10 +++------- 5 files changed, 21 insertions(+), 34 deletions(-) delete mode 100644 pms_api_rest/datamodels/pms_transaction_report.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 75bef6942c..84fd467111 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -51,9 +51,8 @@ from . import pms_account_payment_term from . import pms_room_closure_reason -from . import pms_report from . import pms_cash_register -from . import pms_transaction_report +from . import pms_report from . import pms_folio_sale_line from . import pms_invoice_line from . import pms_mail diff --git a/pms_api_rest/datamodels/pms_report.py b/pms_api_rest/datamodels/pms_report.py index 3fea07f6e3..fc410de864 100644 --- a/pms_api_rest/datamodels/pms_report.py +++ b/pms_api_rest/datamodels/pms_report.py @@ -3,6 +3,14 @@ from odoo.addons.datamodel.core import Datamodel -class PmsReportInfo(Datamodel): - _name = "pms.report.info" - pdf = fields.String(required=False, allow_none=True) +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_transaction_report.py b/pms_api_rest/datamodels/pms_transaction_report.py deleted file mode 100644 index fb5d097fa8..0000000000 --- a/pms_api_rest/datamodels/pms_transaction_report.py +++ /dev/null @@ -1,16 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsTransactionReportSearchParam(Datamodel): - _name = "pms.transaction.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 PmsTransactionReportInput(Datamodel): - _name = "pms.transaction.report" - fileName = fields.String(required=False, allow_none=True) - binary = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 72b61fae9f..a8c4affad0 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -730,7 +730,7 @@ def mapping_checkin_partner_values(self, pms_checkin_partner_info): ) ], auth="jwt_api_pms", - output_param=Datamodel("pms.report.info", is_list=False), + output_param=Datamodel("pms.report", is_list=False), ) def print_all_checkins(self, reservation_id): reservations = False @@ -745,8 +745,8 @@ def print_all_checkins(self, reservation_id): ._render_qweb_pdf(checkins.ids)[0] ) base64EncodedStr = base64.b64encode(pdf) - PmsResponse = self.env.datamodels["pms.report.info"] - return PmsResponse(pdf=base64EncodedStr) + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(binary=base64EncodedStr) @restapi.method( [ @@ -759,7 +759,7 @@ def print_all_checkins(self, reservation_id): ) ], auth="jwt_api_pms", - output_param=Datamodel("pms.report.info", is_list=False), + output_param=Datamodel("pms.report", is_list=False), ) def print_checkin(self, reservation_id, checkin_partner_id): reservations = False @@ -774,5 +774,5 @@ def print_checkin(self, reservation_id, checkin_partner_id): ._render_qweb_pdf(checkin_partner.id)[0] ) base64EncodedStr = base64.b64encode(pdf) - PmsResponse = self.env.datamodels["pms.report.info"] - return PmsResponse(pdf=base64EncodedStr) + PmsResponse = self.env.datamodels["pms.report"] + return PmsResponse(binary=base64EncodedStr) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index cd7ca46ccd..f455487c2f 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -365,8 +365,8 @@ def _action_close_cash_session(self, pms_property_id, amount, journal_id, force) "GET", ) ], - input_param=Datamodel("pms.transaction.report.search.param", is_list=False), - output_param=Datamodel("pms.transaction.report", is_list=False), + 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): @@ -383,9 +383,5 @@ def transactions_report(self, pms_transaction_report_search_param): result = report_wizard._export() file_name = result["xls_filename"] base64EncodedStr = result["xls_binary"] - PmsResponse = self.env.datamodels["pms.transaction.report"] - # REVIEW: Reuse pms.report.info by modifying the fields - # to support different types of documents? - # proposal: contentBase64 = fields.String, - # fileType = fields.String (pdf, xlsx, etc...) + PmsResponse = self.env.datamodels["pms.report"] return PmsResponse(fileName=file_name, binary=base64EncodedStr) From 5f0d5a9511f6994ccd7a822a8f798ed2125ddbb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 7 Nov 2022 10:00:21 +0100 Subject: [PATCH 247/547] [FIX]pms_api_rest: compute suppliet payments types api pms --- pms_api_rest/models/account_payment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/models/account_payment.py b/pms_api_rest/models/account_payment.py index 5d5eb8750b..d5c3a39a5c 100644 --- a/pms_api_rest/models/account_payment.py +++ b/pms_api_rest/models/account_payment.py @@ -28,7 +28,7 @@ def _compute_pms_api_transaction_type(self): else: record.pms_api_transaction_type = "customer_outbound" elif record.partner_type == "supplier": - if record.payment_type == "inbound": + if record.payment_type == "outbound": record.pms_api_transaction_type = "supplier_outbound" else: record.pms_api_transaction_type = "supplier_inbound" From 3f1388fd4b14012332bebf500fa63fa5c9136909 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 7 Nov 2022 20:51:20 +0100 Subject: [PATCH 248/547] [IMP] pms_api_rest: change dates format --- pms_api_rest/services/pms_transaction_service.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index f455487c2f..3a08de1570 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -111,7 +111,6 @@ def get_transactions(self, pms_transactions_search_param): 0, ) amount_result = total_inbound - total_outbound - amount_result = total_inbound - total_outbound for transaction in self.env["account.payment"].search( domain, order=pms_transactions_search_param.orderBy, @@ -186,7 +185,7 @@ def get_transaction(self, transaction_id): input_param=Datamodel("pms.transaction.info", is_list=False), auth="jwt_api_pms", ) - def create_transaction(self): + def create_transaction(self, pms_transaction_info): return True @restapi.method( @@ -237,9 +236,9 @@ def get_cash_register(self, cash_register_search_param): state="open" if isOpen else "close", userId=statement.user_id.id, balance=statement.balance_start if isOpen else statement.balance_end_real, - dateTime=statement.create_date.strftime("%d/%m/%Y") + dateTime=statement.create_date.isoformat() if isOpen - else statement.date_done.strftime("%d/%m/%Y") + else statement.date_done.isoformat() if statement.date_done else None, ) @@ -248,7 +247,7 @@ def get_cash_register(self, cash_register_search_param): [ ( [ - "/p/cash-register", + "/cash-register", ], "POST", ) From 4fecfaf6deb6f9dd0dd68bd7706c5516e40d2721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 8 Nov 2022 10:03:59 +0100 Subject: [PATCH 249/547] [ADD]pms_api_rest: transactions post and get --- pms_api_rest/models/account_payment.py | 5 ++ .../services/pms_transaction_service.py | 87 ++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/models/account_payment.py b/pms_api_rest/models/account_payment.py index d5c3a39a5c..2d92d5b255 100644 --- a/pms_api_rest/models/account_payment.py +++ b/pms_api_rest/models/account_payment.py @@ -16,6 +16,11 @@ class AccountPayment(models.Model): help="Transaction type for PMS API", compute="_compute_pms_api_transaction_type", ) + internal_transfer_id = fields.Many2one( + "account.payment", + string="Internal Transfer Relation", + help="Internal transfer relation", + ) @api.depends("payment_type", "partner_type") def _compute_pms_api_transaction_type(self): diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 3a08de1570..075f3033b5 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -31,7 +31,17 @@ class PmsTransactionService(Component): ) def get_transactions(self, pms_transactions_search_param): result_transactions = [] - domain_fields = [("state", "=", "posted")] + # In internal transfer payments, the APP only show + # the output payment, with the countrapart journal id + # (destinationJournalId), the domain ensure avoid + # get the input internal transfer payment + domain_fields = [ + ("state", "=", "posted"), + "|", + ("is_internal_transfer", "=", False), + ("payment_type", "=", "outbound"), + ] + available_journals = () if pms_transactions_search_param.pmsPropertyId: available_journals = self.env["account.journal"].search( @@ -117,6 +127,9 @@ def get_transactions(self, pms_transactions_search_param): limit=pms_transactions_search_param.limit, offset=pms_transactions_search_param.offset, ): + destination_journal_id = False + if transaction.is_internal_transfer: + destination_journal_id = transaction.internal_transfer_id.journal_id.id result_transactions.append( PmsTransactiontInfo( id=transaction.id, @@ -125,6 +138,7 @@ def get_transactions(self, pms_transactions_search_param): journalId=transaction.journal_id.id if transaction.journal_id else None, + destinationJournalId=destination_journal_id or None, date=transaction.date.strftime("%d/%m/%Y"), partnerId=transaction.partner_id.id if transaction.partner_id @@ -160,11 +174,15 @@ def get_transactions(self, pms_transactions_search_param): 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.internal_transfer_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=transaction.date.strftime("%d/%m/%Y"), partnerId=transaction.partner_id.id if transaction.partner_id else None, partnerName=transaction.partner_id.name if transaction.partner_id else None, @@ -186,7 +204,57 @@ def get_transaction(self, transaction_id): auth="jwt_api_pms", ) def create_transaction(self, pms_transaction_info): - return True + # TODO: FIX fron send data format ('%Y-%m-%d') + # use fields.Date.from_string(pms_transaction_info.date) + pay_date_wrong_format = pms_transaction_info.date + pay_date = datetime.strptime(pay_date_wrong_format, "%m/%d/%Y") + 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) + pay.sudo().action_post() + if is_internal_transfer: + 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.internal_transfer_id = countrepart_pay.id + return pay.id @restapi.method( [ @@ -201,7 +269,7 @@ def create_transaction(self, pms_transaction_info): auth="jwt_api_pms", ) def update_transaction(self, transaction_id): - return True + return transaction_id @restapi.method( [ @@ -384,3 +452,16 @@ def transactions_report(self, pms_transaction_report_search_param): 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" From 12eca3da6ab3e33e8a77770d450464f865f1957e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 8 Nov 2022 10:18:14 +0100 Subject: [PATCH 250/547] [RFC]pms_api_rest: transaction services format dates --- pms_api_rest/services/pms_transaction_service.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 075f3033b5..90ddda3f6f 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -139,7 +139,9 @@ def get_transactions(self, pms_transactions_search_param): if transaction.journal_id else None, destinationJournalId=destination_journal_id or None, - date=transaction.date.strftime("%d/%m/%Y"), + date=datetime.combine( + transaction.date, datetime.min.time() + ).isoformat(), partnerId=transaction.partner_id.id if transaction.partner_id else None, @@ -183,7 +185,7 @@ def get_transaction(self, transaction_id): amount=transaction.amount, journalId=transaction.journal_id.id if transaction.journal_id else None, destinationJournalId=destination_journal_id or None, - date=transaction.date.strftime("%d/%m/%Y"), + 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, @@ -438,8 +440,12 @@ def _action_close_cash_session(self, pms_property_id, amount, journal_id, force) ) def transactions_report(self, pms_transaction_report_search_param): pms_property_id = pms_transaction_report_search_param.pmsPropertyId - date_from = pms_transaction_report_search_param.dateFrom - date_to = pms_transaction_report_search_param.dateTo + date_from = ( + datetime.strptime(pms_transaction_report_search_param.dateFrom, "%Y-%m-%d"), + ) + date_to = ( + datetime.strptime(pms_transaction_report_search_param.dateTo, "%Y-%m-%d"), + ) report_wizard = self.env["cash.daily.report.wizard"].create( { "date_start": date_from, From f256531e60a8088425859ce2f980a00eca7d3de5 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 8 Nov 2022 14:12:44 +0100 Subject: [PATCH 251/547] [IMP]pms_api_rest: added internal_commit in folio GET and POST services --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index c017a9087d..313c3c5ff8 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -35,6 +35,7 @@ class PmsFolioInfo(Datamodel): externalReference = fields.String(required=False, allow_none=True) closureReasonId = fields.Integer(required=False, allow_none=True) preconfirm = fields.Boolean(required=False, allow_none=True) + internalComment = fields.String(required=False, allow_none=True) class PmsFolioShortInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 18b44038fd..d20d1f4f80 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -48,6 +48,9 @@ def get_folio(self, folio_id): reservationType=folio.reservation_type, pendingAmount=folio.pending_amount, lastCheckout=str(folio.last_checkout), + internalComment=folio.internal_comment + if folio.internal_comment + else None, ) else: raise MissingError(_("Folio not found")) @@ -360,6 +363,7 @@ def create_folio(self, pms_folio_info): if pms_folio_info.agencyId else False, "reservation_type": pms_folio_info.reservationType, + "internal_comment": pms_folio_info.internalComment, } if pms_folio_info.partnerId: vals.update( From 82e4bd4a5153a1c8844fd8b06c67a7681b96dfd5 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 8 Nov 2022 11:44:57 +0100 Subject: [PATCH 252/547] [FIX] pms_api_rest: fix date format --- pms_api_rest/services/pms_transaction_service.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 90ddda3f6f..318a549e5e 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -206,10 +206,7 @@ def get_transaction(self, transaction_id): auth="jwt_api_pms", ) def create_transaction(self, pms_transaction_info): - # TODO: FIX fron send data format ('%Y-%m-%d') - # use fields.Date.from_string(pms_transaction_info.date) - pay_date_wrong_format = pms_transaction_info.date - pay_date = datetime.strptime(pay_date_wrong_format, "%m/%d/%Y") + pay_date = fields.Date.from_string(pms_transaction_info.date) payment_type, partner_type = self._get_mapper_transaction_type( pms_transaction_info.transactionType ) From 6f1af916d3183858c18485192bd7141cc3d5f75e Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 8 Nov 2022 17:07:30 +0100 Subject: [PATCH 253/547] [FIX] pms_api_rest: fix date format in transaction report service --- pms_api_rest/services/pms_transaction_service.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 318a549e5e..2863594826 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -437,12 +437,9 @@ def _action_close_cash_session(self, pms_property_id, amount, journal_id, force) ) def transactions_report(self, pms_transaction_report_search_param): pms_property_id = pms_transaction_report_search_param.pmsPropertyId - date_from = ( - datetime.strptime(pms_transaction_report_search_param.dateFrom, "%Y-%m-%d"), - ) - date_to = ( - datetime.strptime(pms_transaction_report_search_param.dateTo, "%Y-%m-%d"), - ) + 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, From ec330d8ef5d02e22899f47adb3635de7813acd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 8 Nov 2022 18:41:00 +0100 Subject: [PATCH 254/547] [IMP]pms_api_rest: compute total transaction with internal transfers --- pms_api_rest/models/account_payment.py | 8 +-- .../services/pms_transaction_service.py | 54 ++++++++++++------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/pms_api_rest/models/account_payment.py b/pms_api_rest/models/account_payment.py index 2d92d5b255..364a33bcfb 100644 --- a/pms_api_rest/models/account_payment.py +++ b/pms_api_rest/models/account_payment.py @@ -16,10 +16,10 @@ class AccountPayment(models.Model): help="Transaction type for PMS API", compute="_compute_pms_api_transaction_type", ) - internal_transfer_id = fields.Many2one( - "account.payment", - string="Internal Transfer Relation", - help="Internal transfer relation", + 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") diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 2863594826..8f95f0c04b 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -31,15 +31,8 @@ class PmsTransactionService(Component): ) def get_transactions(self, pms_transactions_search_param): result_transactions = [] - # In internal transfer payments, the APP only show - # the output payment, with the countrapart journal id - # (destinationJournalId), the domain ensure avoid - # get the input internal transfer payment domain_fields = [ ("state", "=", "posted"), - "|", - ("is_internal_transfer", "=", False), - ("payment_type", "=", "outbound"), ] available_journals = () @@ -73,21 +66,22 @@ def get_transactions(self, pms_transactions_search_param): 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.transactionMethodId: domain_fields.append( - ("journal_id", "=", pms_transactions_search_param.transactionMethodId) + ("journal_id", "=", pms_transactions_search_param.transactionMethodId), ) if pms_transactions_search_param.transactionType: domain_fields.append( - "pms_api_transaction_type", - "=", - pms_transactions_search_param.transactionType, + ( + "pms_api_transaction_type", + "=", + pms_transactions_search_param.transactionType, + ) ) if domain_filter: @@ -97,7 +91,6 @@ def get_transactions(self, pms_transactions_search_param): 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"] @@ -121,15 +114,34 @@ def get_transactions(self, pms_transactions_search_param): 0, ) amount_result = total_inbound - total_outbound - for transaction in self.env["account.payment"].search( + transactions = self.env["account.payment"].search( domain, order=pms_transactions_search_param.orderBy, 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: - destination_journal_id = transaction.internal_transfer_id.journal_id.id + 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 + ) + if outbound_transaction: + transaction = outbound_transaction + if inbound_transaction: + destination_journal_id = inbound_transaction.journal_id.id + result_transactions.append( PmsTransactiontInfo( id=transaction.id, @@ -178,7 +190,9 @@ def get_transaction(self, transaction_id): transaction = self.env["account.payment"].browse(transaction_id) destination_journal_id = False if transaction.is_internal_transfer: - destination_journal_id = transaction.internal_transfer_id.journal_id.id + 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, @@ -252,7 +266,7 @@ def create_transaction(self, pms_transaction_info): } countrepart_pay = self.env["account.payment"].create(counterpart_vals) countrepart_pay.sudo().action_post() - pay.internal_transfer_id = countrepart_pay.id + pay.pms_api_counterpart_payment_id = countrepart_pay.id return pay.id @restapi.method( @@ -437,7 +451,9 @@ def _action_close_cash_session(self, pms_property_id, amount, journal_id, force) ) 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_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( From 856864a0f1eb9f0dcdfe5eedb6c52f6b65a43ac0 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 8 Nov 2022 17:58:55 +0100 Subject: [PATCH 255/547] [IMP]pms-api_rest: added folio PATCH to write internal_comment --- pms_api_rest/services/pms_folio_service.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d20d1f4f80..aee4515401 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -432,6 +432,25 @@ def create_folio(self, pms_folio_info): return folio.id + @restapi.method( + [ + ( + [ + "/p/", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.folio.info", is_list=False), + auth="jwt_api_pms", + ) + def update_folio(self, folio_id, pms_folio_info): + folio = self.env["pms.folio"].browse(folio_id) + if folio: + folio.write({"internal_comment": pms_folio_info.internalComment}) + else: + raise MissingError(_("Folio not found")) + @restapi.method( [ ( From 668652dbf541424087595a497a5fe98e2c988836 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 9 Nov 2022 14:29:11 +0100 Subject: [PATCH 256/547] [IMP]pms_api_rest: filter journals for property in GET transactions services --- .../services/pms_account_journal_service.py | 8 +++---- .../services/pms_transaction_service.py | 24 +++++++------------ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/pms_api_rest/services/pms_account_journal_service.py b/pms_api_rest/services/pms_account_journal_service.py index 4ce6d9aaeb..1b617a9ac8 100644 --- a/pms_api_rest/services/pms_account_journal_service.py +++ b/pms_api_rest/services/pms_account_journal_service.py @@ -31,10 +31,10 @@ def get_method_payments(self, account_journal_search_param): if not pms_property: pass else: - for method in pms_property._get_payment_methods(automatic_included=True): - payment_method = self.env["account.journal"].search( - [("id", "=", method.id)] - ) + 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, diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 8f95f0c04b..937ad7d222 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -35,18 +35,16 @@ def get_transactions(self, pms_transactions_search_param): ("state", "=", "posted"), ] - available_journals = () - if pms_transactions_search_param.pmsPropertyId: - available_journals = self.env["account.journal"].search( - [ - ( - "pms_property_ids", - "in", - pms_transactions_search_param.pmsPropertyId, - ), - ] + if pms_transactions_search_param.transactionMethodId: + domain_fields.append( + ("journal_id", "=", pms_transactions_search_param.transactionMethodId), ) - domain_fields.append(("journal_id", "in", available_journals.ids)) + 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 @@ -70,10 +68,6 @@ def get_transactions(self, pms_transactions_search_param): ("date", "<", date_to), ] ) - if pms_transactions_search_param.transactionMethodId: - domain_fields.append( - ("journal_id", "=", pms_transactions_search_param.transactionMethodId), - ) if pms_transactions_search_param.transactionType: domain_fields.append( From fd317a6f2f270d1e91f856c5079143c5afbec7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 9 Nov 2022 18:22:08 +0100 Subject: [PATCH 257/547] [IMP] Cash flow improvements & internal transactions --- .../services/pms_transaction_service.py | 151 +++++++++++++++--- 1 file changed, 129 insertions(+), 22 deletions(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 937ad7d222..e4cccff042 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -1,5 +1,8 @@ +import logging from datetime import datetime +import pytz + from odoo import _, fields from odoo.exceptions import ValidationError from odoo.osv import expression @@ -9,6 +12,8 @@ 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" @@ -40,10 +45,16 @@ def get_transactions(self, pms_transactions_search_param): ("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) + 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) + 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: @@ -121,6 +132,12 @@ def get_transactions(self, pms_transactions_search_param): # 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" @@ -131,8 +148,11 @@ def get_transactions(self, pms_transactions_search_param): if transaction.payment_type == "inbound" else transaction.pms_api_counterpart_payment_id ) - if outbound_transaction: - transaction = outbound_transaction + transaction = ( + outbound_transaction + if outbound_transaction + else inbound_transaction + ) if inbound_transaction: destination_journal_id = inbound_transaction.journal_id.id @@ -261,6 +281,7 @@ def create_transaction(self, pms_transaction_info): 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( @@ -302,16 +323,19 @@ def get_cash_register(self, cash_register_search_param): limit=1, ) ) - 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) + 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=statement.create_date.isoformat() + dateTime=create_date.isoformat() if isOpen else statement.date_done.isoformat() if statement.date_done @@ -399,32 +423,52 @@ def _action_close_cash_session(self, pms_property_id, amount, journal_id, force) limit=1, ) ) - if round(statement.balance_end, 2) == round(amount, 2): - statement.sudo().balance_end_real = amount - statement.sudo().button_post() + 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: - # 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" + self._session_create_statement_lines( + session_payments, statement, amount, auto_conciliation=False ) - if lines_of_moves_to_post: - lines_of_moves_to_post.move_id._post(soft=False) - diff = round(amount - statement.balance_end, 2) + diff = round(amount - compute_end_balance, 2) return { "result": True, "diff": diff, } else: - diff = round(amount - statement.balance_end, 2) + diff = round(amount - compute_end_balance, 2) return { "result": False, "diff": diff, @@ -475,3 +519,66 @@ def _get_mapper_transaction_type(self, transaction_type): 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() From f7cf214167075b34970c628984e5498d530ad07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 9 Nov 2022 18:32:19 +0100 Subject: [PATCH 258/547] [IMP]pms_api_rest: Cash flow - ensure get last statement --- pms_api_rest/services/pms_transaction_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index e4cccff042..3087b66969 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -320,6 +320,7 @@ def get_cash_register(self, cash_register_search_param): [ ("journal_id", "=", cash_register_search_param.journalId), ], + order="date desc, id desc", limit=1, ) ) From 4bf70b89cdc772b62abd6351670aced8d31e1ba6 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 9 Nov 2022 21:19:09 +0100 Subject: [PATCH 259/547] [IMP] pms_api_rest: change domain in get_transactions to consider correct date --- pms_api_rest/services/pms_transaction_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 3087b66969..598bffcde4 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -76,7 +76,7 @@ def get_transactions(self, pms_transactions_search_param): domain_fields.extend( [ ("date", ">=", date_from), - ("date", "<", date_to), + ("date", "<=", date_to), ] ) From 1d40de2d8b6ce66fd26df9e5ef3d720a3bfc93c4 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 9 Nov 2022 18:28:05 +0100 Subject: [PATCH 260/547] [IMP]pms_api_rest: added filter partners by email and precommit --- pms_api_rest/datamodels/pms_partner.py | 1 - .../services/pms_account_journal_service.py | 4 +++- pms_api_rest/services/pms_partner_service.py | 16 +++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index 4b0c9e03d8..a2252a7cec 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -8,7 +8,6 @@ class PmsPartnerSearchParam(Datamodel): _name = "pms.partner.search.param" _inherit = "pms.rest.metadata" id = fields.Integer(required=False, allow_none=True) - vatNumberOrName = fields.String(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) diff --git a/pms_api_rest/services/pms_account_journal_service.py b/pms_api_rest/services/pms_account_journal_service.py index 1b617a9ac8..8e66c2369f 100644 --- a/pms_api_rest/services/pms_account_journal_service.py +++ b/pms_api_rest/services/pms_account_journal_service.py @@ -31,7 +31,9 @@ def get_method_payments(self, account_journal_search_param): if not pms_property: pass else: - for payment_method in pms_property._get_payment_methods(automatic_included=True): + 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 diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index a8460f9b66..df74b3adb9 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -51,22 +51,20 @@ def get_partners(self, pms_partner_search_params): domain.append(("is_company", "=", False)) domain.append(("is_agency", "=", False)) if pms_partner_search_params.filter: - domain.append(("display_name", "ilike", pms_partner_search_params.filter)) - if pms_partner_search_params.vatNumberOrName: subdomains = [ - [("vat", "ilike", pms_partner_search_params.vatNumberOrName)], + [("vat", "like", pms_partner_search_params.filter)], [ ( "aeat_identification", - "ilike", - pms_partner_search_params.vatNumberOrName, + "like", + pms_partner_search_params.filter, ) ], - [("display_name", "ilike", pms_partner_search_params.vatNumberOrName)], + [("display_name", "like", pms_partner_search_params.filter)], + [("email", "like", pms_partner_search_params.filter)], ] - domain_vat_or_name = expression.OR(subdomains) - domain = expression.AND([domain, domain_vat_or_name]) - + 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) From a113a9eb4632182f5cb969fa02e58d363b267576 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 9 Nov 2022 18:34:15 +0100 Subject: [PATCH 261/547] [FIX]pms_api_rest: change subdomains like by ilike in get_partners --- pms_api_rest/services/pms_partner_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index df74b3adb9..c065c5a02b 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -52,16 +52,16 @@ def get_partners(self, pms_partner_search_params): domain.append(("is_agency", "=", False)) if pms_partner_search_params.filter: subdomains = [ - [("vat", "like", pms_partner_search_params.filter)], + [("vat", "ilike", pms_partner_search_params.filter)], [ ( "aeat_identification", - "like", + "ilike", pms_partner_search_params.filter, ) ], - [("display_name", "like", pms_partner_search_params.filter)], - [("email", "like", pms_partner_search_params.filter)], + [("display_name", "ilike", pms_partner_search_params.filter)], + [("email", "ilike", pms_partner_search_params.filter)], ] domain_partner_search_field = expression.OR(subdomains) domain = expression.AND([domain, domain_partner_search_field]) From b5cc9f91126042b6d8bd4da09c8aa463509e4a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 10 Nov 2022 16:51:52 +0100 Subject: [PATCH 262/547] [FIX]pms_api_rest: fix get last cash session --- .../services/pms_transaction_service.py | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 598bffcde4..642901a5af 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -313,16 +313,8 @@ def update_transaction(self, transaction_id): auth="jwt_api_pms", ) def get_cash_register(self, cash_register_search_param): - statement = ( - self.env["account.bank.statement"] - .sudo() - .search( - [ - ("journal_id", "=", cash_register_search_param.journalId), - ], - order="date desc, id desc", - limit=1, - ) + statement = self._get_last_cash_session( + journal_id=cash_register_search_param.journalId, ) CashRegister = self.env.datamodels["pms.cash.register.info"] if not statement: @@ -382,15 +374,9 @@ def cash_register(self, cash_register_action): ) def _action_open_cash_session(self, pms_property_id, amount, journal_id, force): - statement = ( - self.env["account.bank.statement"] - .sudo() - .search( - [ - ("journal_id", "=", journal_id), - ], - limit=1, - ) + 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( @@ -412,17 +398,9 @@ def _action_open_cash_session(self, pms_property_id, amount, journal_id, force): return {"result": False, "diff": diff} def _action_close_cash_session(self, pms_property_id, amount, journal_id, force): - statement = ( - self.env["account.bank.statement"] - .sudo() - .search( - [ - ("journal_id", "=", journal_id), - ("state", "=", "open"), - ("pms_property_id", "=", pms_property_id), - ], - limit=1, - ) + statement = self._get_last_cash_session( + journal_id=journal_id, + pms_property_id=pms_property_id, ) session_payments = ( self.env["account.payment"] @@ -583,3 +561,17 @@ def _session_create_statement_lines( 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, + ) + ) From fb6c9a07733c6e8023ab8238830a108ae9e90aa5 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 10 Nov 2022 19:56:18 +0100 Subject: [PATCH 263/547] [FIX]pms_api_rest: added discount in folio sale line and invoice GET service --- pms_api_rest/datamodels/pms_folio_sale_line.py | 1 + pms_api_rest/datamodels/pms_invoice_line.py | 1 + pms_api_rest/services/pms_folio_service.py | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio_sale_line.py b/pms_api_rest/datamodels/pms_folio_sale_line.py index 89c5837e60..7c5f0bb498 100644 --- a/pms_api_rest/datamodels/pms_folio_sale_line.py +++ b/pms_api_rest/datamodels/pms_folio_sale_line.py @@ -11,6 +11,7 @@ class PmsFolioSaleInfo(Datamodel): 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) diff --git a/pms_api_rest/datamodels/pms_invoice_line.py b/pms_api_rest/datamodels/pms_invoice_line.py index 29aa784ecd..96532a9dbe 100644 --- a/pms_api_rest/datamodels/pms_invoice_line.py +++ b/pms_api_rest/datamodels/pms_invoice_line.py @@ -10,5 +10,6 @@ class PmsInvoiceLineInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index aee4515401..f7bd79ec5f 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -488,6 +488,7 @@ def get_folio_sale_lines(self, folio_id): 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, @@ -542,6 +543,9 @@ def get_folio_invoices(self, folio_id): 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, From 5797ca545f7c91440d8ef64d99c082c3c7e39534 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 21 Apr 2022 18:40:54 +0200 Subject: [PATCH 264/547] [IMP]pms: added configuration and color code for reservation segments in the property --- pms/models/pms_property.py | 79 ++++++++++++++++++++++++++++++++ pms/views/pms_property_views.xml | 67 +++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index 63ff0ee50c..cb699f7119 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -243,6 +243,85 @@ class PmsProperty(models.Model): default=False, ) + 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)", + ) + @api.depends_context( "checkin", "checkout", diff --git a/pms/views/pms_property_views.xml b/pms/views/pms_property_views.xml index 415f260326..8da488f716 100644 --- a/pms/views/pms_property_views.xml +++ b/pms/views/pms_property_views.xml @@ -88,6 +88,73 @@ + + + + + + + + + + + + + + + + + + + From 811d792a90d5d346cfedda6063add9df58471668 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 11 Nov 2022 15:00:27 +0100 Subject: [PATCH 265/547] [IMP] pms_api_rest: first approach transactions patch with isReconcilied field --- pms_api_rest/datamodels/pms_transaction.py | 1 + pms_api_rest/services/pms_transaction_service.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_transaction.py b/pms_api_rest/datamodels/pms_transaction.py index 5295489336..3d9da640d1 100644 --- a/pms_api_rest/datamodels/pms_transaction.py +++ b/pms_api_rest/datamodels/pms_transaction.py @@ -40,3 +40,4 @@ class PmsTransactionInfo(Datamodel): transactionType = fields.String(required=False, allow_none=True) # REVIEW: Fields to avoid?: partnerName = fields.String(required=False, allow_none=True) + isReconcilied = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 642901a5af..b75d74561b 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -179,6 +179,8 @@ def get_transactions(self, pms_transactions_search_param): if transaction.create_uid else None, transactionType=transaction.pms_api_transaction_type or None, + # TODO: calculate correctly this field + isReconcilied=True, ) ) return PmsTransactionResults( @@ -296,7 +298,7 @@ def create_transaction(self, pms_transaction_info): input_param=Datamodel("pms.transaction.info", is_list=False), auth="jwt_api_pms", ) - def update_transaction(self, transaction_id): + def update_transaction(self, transaction_id, pms_transaction_info): return transaction_id @restapi.method( From 31efefff3794029d1bf688e3ecda585c77da4289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 12 Nov 2022 08:09:37 +0100 Subject: [PATCH 266/547] [ADD]pms_api_rest: transaction PATCH --- .../services/pms_transaction_service.py | 95 ++++++++++++++++++- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index b75d74561b..eacfcbda06 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -4,7 +4,7 @@ import pytz from odoo import _, fields -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.osv import expression from odoo.tools import get_lang @@ -179,8 +179,7 @@ def get_transactions(self, pms_transactions_search_param): if transaction.create_uid else None, transactionType=transaction.pms_api_transaction_type or None, - # TODO: calculate correctly this field - isReconcilied=True, + isReconcilied=transaction.reconciled_statements_count > 0, ) ) return PmsTransactionResults( @@ -221,6 +220,7 @@ def get_transaction(self, transaction_id): 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, ) @restapi.method( @@ -299,7 +299,94 @@ def create_transaction(self, pms_transaction_info): auth="jwt_api_pms", ) def update_transaction(self, transaction_id, pms_transaction_info): - return transaction_id + transaction = self.env["account.payment"].browse(transaction_id) + vals = {} + transacion_type = pms_transaction_info.transactionType + counterpart_transaction = False + # 1- calculo los valores genericos que se cambiaran (amount, partner_id, ref, date) + # 3- si es una transferencia interna hay que modificar el pago de contrapartida + # 2- si se cambia el journal_id habrá que cancelar y crear un nuevo pago + + # Get generic update vals + if pms_transaction_info.amount and round( + pms_transaction_info.amount, 2 + ) != round(transaction.amount, 2): + vals["amount"] = pms_transaction_info.amount + 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 + 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().write(vals) + transaction.sudo().action_post() + if counterpart_transaction: + counterpart_transaction.sudo().write(vals) + counterpart_transaction.sudo().action_post() + return transaction.id @restapi.method( [ From 4155da1680205c0a3d5727982526e677f8fa0646 Mon Sep 17 00:00:00 2001 From: braisab Date: Sat, 12 Nov 2022 17:08:18 +0100 Subject: [PATCH 267/547] [IMP]pms_api_rest: added default_invoice_to field in folio sale line GET service --- pms_api_rest/datamodels/pms_folio_sale_line.py | 1 + pms_api_rest/services/pms_folio_service.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio_sale_line.py b/pms_api_rest/datamodels/pms_folio_sale_line.py index 7c5f0bb498..1d567716d7 100644 --- a/pms_api_rest/datamodels/pms_folio_sale_line.py +++ b/pms_api_rest/datamodels/pms_folio_sale_line.py @@ -16,3 +16,4 @@ class PmsFolioSaleInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index f7bd79ec5f..77e8b4f667 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -501,6 +501,9 @@ def get_folio_sale_lines(self, folio_id): 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, ) ) From 91bab7f18f01dca0fcef37c1dca469143dd25817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 13 Nov 2022 12:35:19 +0100 Subject: [PATCH 268/547] [ADD]pms_api_rest: Add Patch invoices complete --- pms_api_rest/datamodels/pms_invoice.py | 1 + pms_api_rest/services/pms_folio_service.py | 1 + pms_api_rest/services/pms_invoice_service.py | 212 +++++++++++-------- 3 files changed, 124 insertions(+), 90 deletions(-) diff --git a/pms_api_rest/datamodels/pms_invoice.py b/pms_api_rest/datamodels/pms_invoice.py index ab5e0779fc..b0a65b7705 100644 --- a/pms_api_rest/datamodels/pms_invoice.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -19,3 +19,4 @@ class PmsAccountInvoiceInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 77e8b4f667..186c7f8f78 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -588,6 +588,7 @@ def get_folio_invoices(self, folio_id): else None, moveLines=move_lines if move_lines else None, portalUrl=portal_url, + moveType=move.move_type, ) ) return invoices diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 9494659fe0..05726df5bc 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -1,4 +1,5 @@ from odoo import _, fields +from odoo.exceptions import UserError from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -23,14 +24,13 @@ class PmsInvoiceService(Component): 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) - - # Build update values dict - # TODO: Missing data: - # - invoice comment (narration) - # - add new invoice lines (from saleLines selected?) - + 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")) new_vals = {} if ( pms_invoice_info.partnerId @@ -47,97 +47,73 @@ def update_invoice(self, invoice_id, pms_invoice_info): # send to service, the lines that are not sent we assume that # they have been eliminated if pms_invoice_info.moveLines and pms_invoice_info.moveLines is not None: - new_vals["invoice_line_ids"] = [] - 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 and line_info.name != line.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: - new_vals["invoice_line_ids"].append((1, line.id, line_values)) - else: - new_vals["invoice_line_ids"].append((2, line.id)) - # Get the new lines to add in invoice - new_invoice_lines_info = list( - filter(lambda item: not item.id, pms_invoice_info.moveLines) + cmd_invoice_lines = self._get_invoice_lines_commands( + invoice, pms_invoice_info ) - if new_invoice_lines_info: - 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( + if cmd_invoice_lines: + new_vals["invoice_line_ids"] = cmd_invoice_lines + if new_vals: + # Update Invoice + # When modifying an invoice, depending on the company's configuration, + # and the invoice stateit 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 != "draft": + # invoice create refund + # new invoice with new_vals + move_reversal = ( + self.env["account.move.reversal"] + .with_context(active_model="account.move", active_ids=invoice.ids) + .create( { - self.env["folio.sale.line"] - .browse(line.saleLineId) - .folio_id.id - for line in list( - filter( - lambda item: item.name, - pms_invoice_info.moveLines, - ) - ) + "date": fields.Date.today(), + "reason": _("Invoice modification"), + "refund_method": "modify", } ) ) - new_vals["invoice_line_ids"].extend( - [ - item["invoice_line_ids"] - for item in folios.get_invoice_vals_list( - lines_to_invoice={ - new_invoice_lines_info[i] - .saleLineId: new_invoice_lines_info[i] - .quantity - for i in range(0, len(new_invoice_lines_info)) - }, - partner_invoice_id=partner.id, - ) - ][0] - ) - if not new_vals: - return invoice.id - - # Update Invoice - # When modifying an invoice, depending on the company's configuration, - # and the invoice stateit 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 != "draft": - # invoice create refund - # new invoice with new_vals - move_reversal = ( - self.env["account.move.reversal"] - .with_context(active_model="account.move", active_ids=invoice.ids) - .create( - { - "date": fields.Date.today(), - "reason": _("Invoice modification"), - "refund_method": "modify", - } + reversal_action = move_reversal.reverse_moves() + reverse_invoice = self.env["account.move"].browse( + reversal_action["res_id"] ) - ) - reversal_action = move_reversal.reverse_moves() - reverse_invoice = self.env["account.move"].browse(reversal_action["res_id"]) - invoice = reverse_invoice + invoice = reverse_invoice + invoice.sudo().action_post() + # If change invoice by reversal, and new_vals has invoice_line_ids + # we need to mapp the new invoice lines with the new invoice + reverse_lines = [] + for line in new_vals["invoice_line_ids"]: + origin_line = self.env["account.move.line"].browse(line[1]) + sale_line_id = origin_line.sale_line_ids.id + reverse_line = reverse_invoice.invoice_line_ids.filtered( + lambda item: item.sale_line_ids.id == sale_line_id + and item.price_unit == origin_line.price_unit + and item.quantity == origin_line.quantity + ) + if line[0] == 2: + reverse_lines.append((2, reverse_line[0].id)) + elif line[0] == 1: + reverse_lines.append((1, reverse_line[0].id, line)) + else: + reverse_lines.append(line) + if reverse_lines: + new_vals["invoice_line_ids"] = reverse_lines - invoice = self._direct_move_update(invoice, new_vals) - # Update invoice lines name - for item in pms_invoice_info.moveLines: - if item.saleLineId in invoice.invoice_line_ids.mapped("folio_line_ids.id"): - invoice_line = invoice.invoice_line_ids.filtered( - lambda r: item.saleLineId in r.folio_line_ids.ids - ) - invoice_line.write({"name": item.name}) + invoice = self._direct_move_update(invoice, new_vals) + # Update invoice lines name + for item in pms_invoice_info.moveLines: + if item.saleLineId in invoice.invoice_line_ids.mapped( + "folio_line_ids.id" + ): + invoice_line = invoice.invoice_line_ids.filtered( + lambda r: item.saleLineId in r.folio_line_ids.ids + ) + invoice_line.write({"name": item.name}) + if pms_invoice_info.narration is not None: + invoice.write({"narration": pms_invoice_info.narration}) + if invoice.state == "draft" and pms_invoice_info.state == "confirm": + invoice.action_post() return invoice.id def _direct_move_update(self, invoice, new_vals): @@ -185,3 +161,59 @@ def send_invoice_mail(self, invoice_id, pms_mail_info): } template.send_mail(invoice.id, force_send=True, email_values=email_values) 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 and line_info.name != line.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)) + else: + cmd_invoice_lines.append((2, line.id)) + # Get the new lines to add in invoice + new_invoice_lines_info = list( + filter(lambda item: not item.id, pms_invoice_info.moveLines) + ) + if new_invoice_lines_info: + 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, + ) + ) + } + ) + ) + cmd_invoice_lines.extend( + [ + item["invoice_line_ids"] + for item in folios.get_invoice_vals_list( + lines_to_invoice={ + new_invoice_lines_info[i] + .saleLineId: new_invoice_lines_info[i] + .quantity + for i in range(0, len(new_invoice_lines_info)) + }, + partner_invoice_id=partner.id, + ) + ][0] + ) + return cmd_invoice_lines From e3b5b5321bd48e0420f1a3302cf2e268bc041e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 13 Nov 2022 21:27:47 +0100 Subject: [PATCH 269/547] [ADD]pms_api_rest: autoinvoice downpayments policy --- pms_api_rest/services/pms_invoice_service.py | 22 +++++++++++-------- .../services/pms_transaction_service.py | 10 +++++++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 05726df5bc..cf2b72031c 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -74,16 +74,12 @@ def update_invoice(self, invoice_id, pms_invoice_info): } ) ) - reversal_action = move_reversal.reverse_moves() - reverse_invoice = self.env["account.move"].browse( - reversal_action["res_id"] - ) - invoice = reverse_invoice - invoice.sudo().action_post() + move_reversal.reverse_moves() + reverse_invoice = move_reversal.new_move_ids # If change invoice by reversal, and new_vals has invoice_line_ids # we need to mapp the new invoice lines with the new invoice reverse_lines = [] - for line in new_vals["invoice_line_ids"]: + for line in new_vals.get("invoice_line_ids", []): origin_line = self.env["account.move.line"].browse(line[1]) sale_line_id = origin_line.sale_line_ids.id reverse_line = reverse_invoice.invoice_line_ids.filtered( @@ -99,8 +95,16 @@ def update_invoice(self, invoice_id, pms_invoice_info): reverse_lines.append(line) if reverse_lines: new_vals["invoice_line_ids"] = reverse_lines - - invoice = self._direct_move_update(invoice, new_vals) + new_vals["journal_id"] = ( + invoice.pms_property_id._get_folio_default_journal( + new_vals.get("partner_id", invoice.partner_id.id) + ).id, + ) + reverse_invoice.write(new_vals) + invoice = reverse_invoice + invoice.sudo().action_post() + else: + invoice = self._direct_move_update(invoice, new_vals) # Update invoice lines name for item in pms_invoice_info.moveLines: if item.saleLineId in invoice.invoice_line_ids.mapped( diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index eacfcbda06..a186bdd872 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -179,7 +179,10 @@ def get_transactions(self, pms_transactions_search_param): if transaction.create_uid else None, transactionType=transaction.pms_api_transaction_type or None, - isReconcilied=transaction.reconciled_statements_count > 0, + isReconcilied=( + transaction.reconciled_statements_count > 0 + or transaction.reconciled_invoices_count > 0 + ), ) ) return PmsTransactionResults( @@ -220,7 +223,10 @@ def get_transaction(self, transaction_id): 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, + isReconcilied=( + transaction.reconciled_statements_count > 0 + or transaction.reconciled_invoices_count > 0 + ), ) @restapi.method( From 133fc54150939285888e77127cf7cc2449cad88a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 14 Nov 2022 14:01:58 +0100 Subject: [PATCH 270/547] [IMP]pms_api_rest: set journal id by partner in reverse moves --- pms_api_rest/services/pms_folio_service.py | 8 ++++++-- pms_api_rest/services/pms_invoice_service.py | 5 +++++ pms_api_rest/services/pms_transaction_service.py | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 186c7f8f78..fd5e066564 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -232,7 +232,9 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): if pms_account_payment_info.reservationIds else False ) - self.env["pms.folio"].do_payment( + # TODO: no_cash_register context to maintain compatibility + # with older versions, delete it in the future + self.env["pms.folio"].with_context(no_cash_register=True).do_payment( journal_id, journal_id.suspense_account_id, self.env.user, @@ -262,7 +264,9 @@ def create_folio_refund(self, folio_id, pms_account_payment_info): journal_id = self.env["account.journal"].browse( pms_account_payment_info.journalId ) - self.env["pms.folio"].do_refund( + # TODO: no_cash_register context to maintain compatibility + # with older versions, delete it in the future + self.env["pms.folio"].with_context(no_cash_register=True).do_refund( journal_id, journal_id.suspense_account_id, self.env.user, diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index cf2b72031c..06ce46c649 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -76,6 +76,11 @@ def update_invoice(self, invoice_id, pms_invoice_info): ) move_reversal.reverse_moves() reverse_invoice = move_reversal.new_move_ids + invoice = reverse_invoice + invoice.journal_id = invoice.pms_property_id._get_folio_default_journal( + partner_invoice_id=new_vals.get("partner_id", invoice.partner_id.id) + ) + invoice.sudo().action_post() # If change invoice by reversal, and new_vals has invoice_line_ids # we need to mapp the new invoice lines with the new invoice reverse_lines = [] diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index a186bdd872..0c19f2bfd1 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -387,6 +387,7 @@ def update_transaction(self, transaction_id, pms_transaction_info): 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: From ad853a7869e53a9e7a1242655ef73244141a7c85 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 9 Nov 2022 13:06:53 +0100 Subject: [PATCH 271/547] [IMP] pms-api-rest: add default fields to room types (quota & max-avail) & fix -1 as a default value --- pms_api_rest/datamodels/pms_room_type.py | 2 ++ pms_api_rest/services/pms_availability_plan_service.py | 6 ++---- pms_api_rest/services/pms_room_type_service.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/datamodels/pms_room_type.py b/pms_api_rest/datamodels/pms_room_type.py index 48c4370baf..b96b869eb0 100644 --- a/pms_api_rest/datamodels/pms_room_type.py +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -17,3 +17,5 @@ class PmsRoomTypeInfo(Datamodel): defaultCode = fields.String(required=False, allow_none=True) classId = fields.Integer(required=False, allow_none=True) price = 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/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index a6acb68519..d1b20f9540 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -171,10 +171,8 @@ def get_availability_plan_rules( closed=rule["closed"], closedDeparture=rule["closed_departure"], closedArrival=rule["closed_arrival"], - quota=rule["quota"] if rule["quota"] != -1 else 0, - maxAvailability=rule["max_avail"] - if rule["max_avail"] != -1 - else 0, + quota=rule["quota"], + maxAvailability=rule["max_avail"], availabilityPlanId=availability_plan_id, ) result.append(availability_plan_rule_info) diff --git a/pms_api_rest/services/pms_room_type_service.py b/pms_api_rest/services/pms_room_type_service.py index 6f5dc5c25c..d2205cad17 100644 --- a/pms_api_rest/services/pms_room_type_service.py +++ b/pms_api_rest/services/pms_room_type_service.py @@ -61,6 +61,8 @@ def get_room_types(self, room_type_search_param): defaultCode=room.default_code, price=round(room.list_price, 2), classId=room.class_id, + defaultMaxAvail=room.default_max_avail, + defaultQuota=room.default_quota, ) ) return result_rooms From ae6f3029ea460fdff48be2e2b15d5cd9bde90f27 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 14 Nov 2022 14:40:40 +0100 Subject: [PATCH 272/547] [IMP] pms_api_rest: add fields in get transactions of folio --- pms_api_rest/services/pms_folio_service.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index fd5e066564..c43432289d 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -205,6 +205,17 @@ def get_folio_transactions(self, folio_id, pms_search_param): 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 + or payment.reconciled_invoices_count > 0 + ), ) ) return transactions From 5b5597a625129a18bbe8d72f2ab866bcef08eded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 14 Nov 2022 19:19:14 +0100 Subject: [PATCH 273/547] [IMP]pms_api_rest: filtered payments folios by state --- pms_api_rest/services/pms_folio_service.py | 4 +++- pms_api_rest/services/pms_invoice_service.py | 4 ---- pms_api_rest/services/pms_transaction_service.py | 5 +---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index c43432289d..f533c9779f 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -194,7 +194,9 @@ def get_folio_transactions(self, folio_id, pms_search_param): # pass # else: if folio.payment_ids: - for payment in folio.payment_ids: + for payment in folio.payment_ids.filtered( + lambda p: p.state == "posted" + ): payment._compute_pms_api_transaction_type() transactions.append( PmsTransactiontInfo( diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 06ce46c649..d1bffbe0d1 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -77,10 +77,6 @@ def update_invoice(self, invoice_id, pms_invoice_info): move_reversal.reverse_moves() reverse_invoice = move_reversal.new_move_ids invoice = reverse_invoice - invoice.journal_id = invoice.pms_property_id._get_folio_default_journal( - partner_invoice_id=new_vals.get("partner_id", invoice.partner_id.id) - ) - invoice.sudo().action_post() # If change invoice by reversal, and new_vals has invoice_line_ids # we need to mapp the new invoice lines with the new invoice reverse_lines = [] diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 0c19f2bfd1..6b872816f2 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -309,10 +309,7 @@ def update_transaction(self, transaction_id, pms_transaction_info): vals = {} transacion_type = pms_transaction_info.transactionType counterpart_transaction = False - # 1- calculo los valores genericos que se cambiaran (amount, partner_id, ref, date) - # 3- si es una transferencia interna hay que modificar el pago de contrapartida - # 2- si se cambia el journal_id habrá que cancelar y crear un nuevo pago - + # TODO: Downpayment invoiced (search invoice, reverse it and create a new one) # Get generic update vals if pms_transaction_info.amount and round( pms_transaction_info.amount, 2 From 4fd8f7e077214da12e833a48df15050b3fbb3d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 15 Nov 2022 01:18:12 +0100 Subject: [PATCH 274/547] [FIX]pms_api_rest: ubications&room_type_class GET services by property --- .../services/pms_room_type_class_service.py | 42 ++++++++----------- .../services/pms_ubication_service.py | 31 +++++--------- 2 files changed, 27 insertions(+), 46 deletions(-) diff --git a/pms_api_rest/services/pms_room_type_class_service.py b/pms_api_rest/services/pms_room_type_class_service.py index 876ac23f93..8566d6d6e3 100644 --- a/pms_api_rest/services/pms_room_type_class_service.py +++ b/pms_api_rest/services/pms_room_type_class_service.py @@ -23,36 +23,28 @@ class PmsRoomTypeClassService(Component): auth="jwt_api_pms", ) def get_room_type_class(self, room_type_class_search_param): - room_type_class_all_properties = self.env["pms.room.type.class"].search( - [("pms_property_ids", "=", False)] - ) if room_type_class_search_param.pmsPropertyIds: - room_type_class = set() - for index, prop in enumerate(room_type_class_search_param.pmsPropertyds): - room_type_class_with_query_property = self.env[ - "pms.room.type.class" - ].search([("pms_property_ids", "=", prop)]) - if index == 0: - room_type_class = set(room_type_class_with_query_property.ids) - else: - room_type_class = room_type_class.intersection( - set(room_type_class_with_query_property.ids) - ) - room_type_class_total = list( - set(list(room_type_class) + room_type_class_all_properties.ids) + 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_class_total = list(room_type_class_all_properties.ids) - domain = [ - ("id", "in", room_type_class_total), - ] - + 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 self.env["pms.room.type.class"].search( - domain, - ): - + for room in room_type_classes: result_room_type_class.append( PmsRoomTypeClassInfo( id=room.id, diff --git a/pms_api_rest/services/pms_ubication_service.py b/pms_api_rest/services/pms_ubication_service.py index 124dd30e40..4e47d362ad 100644 --- a/pms_api_rest/services/pms_ubication_service.py +++ b/pms_api_rest/services/pms_ubication_service.py @@ -23,33 +23,22 @@ class PmsUbicationService(Component): auth="jwt_api_pms", ) def get_ubications(self, ubication_search_param): - ubication_all_properties = self.env["pms.ubication"].search( - [("pms_property_ids", "=", False)] - ) if ubication_search_param.pmsPropertyIds: - ubication = set() - for index, prop in enumerate(ubication_search_param.pmsPropertyIds): - ubication_with_query_property = self.env["pms.ubication"].search( - [("pms_property_ids", "=", prop)] + ubications = ( + self.env["pms.room"] + .search( + [("pms_property_id", "in", ubication_search_param.pmsPropertyIds)] ) - if index == 0: - ubication = set(ubication_with_query_property.ids) - else: - ubication = ubication.intersection( - set(ubication_with_query_property.ids) - ) - ubication_total = list(set(list(ubication) + ubication_all_properties.ids)) + .mapped("ubication_id") + ) else: - ubication_total = list(ubication_all_properties.ids) - domain = [ - ("id", "in", ubication_total), - ] + ubications = self.env["pms.ubication"].search( + [("pms_property_ids", "=", False)] + ) result_ubications = [] PmsUbicationInfo = self.env.datamodels["pms.ubication.info"] - for ubication in self.env["pms.ubication"].search( - domain, - ): + for ubication in ubications: result_ubications.append( PmsUbicationInfo( From 6bbf76773f0473f3e26b36d5d6182544237327c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 17 Nov 2022 10:08:48 +0100 Subject: [PATCH 275/547] [FIX]pms_api_rest: fix room id integer --- pms_api_rest/services/pms_reservation_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index a8c4affad0..65be7c7be1 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -227,7 +227,7 @@ def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): ): line_vals["cancel_discount"] = origin_data.cancelDiscount if origin_data.roomId and ( - not reservation_line or origin_data.roomId != reservation_line.room_id + not reservation_line or origin_data.roomId != reservation_line.room_id.id ): line_vals["room_id"] = origin_data.roomId return line_vals From 8bdf9780fbbe35e294003f521b4cb4dd70aa59de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 17 Nov 2022 10:12:18 +0100 Subject: [PATCH 276/547] [WIP]pms_api_rest: add field sendConfirmationMail to folio info datamodel --- pms_api_rest/datamodels/pms_folio.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 313c3c5ff8..b807bec7f8 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -36,6 +36,8 @@ class PmsFolioInfo(Datamodel): closureReasonId = fields.Integer(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) class PmsFolioShortInfo(Datamodel): From 71ec31bb1b7af0568c83b69f1850cd85c030776a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 16 Nov 2022 09:39:56 +0100 Subject: [PATCH 277/547] [WIP]pms_api_rest: improvemente invoice&payment datamodels: reversed, downpayments... --- pms_api_rest/datamodels/pms_invoice.py | 4 +++ pms_api_rest/datamodels/pms_transaction.py | 4 +-- pms_api_rest/services/pms_folio_service.py | 9 ++++--- pms_api_rest/services/pms_partner_service.py | 25 +++++++++++++++---- .../services/pms_transaction_service.py | 12 ++++----- 5 files changed, 38 insertions(+), 16 deletions(-) diff --git a/pms_api_rest/datamodels/pms_invoice.py b/pms_api_rest/datamodels/pms_invoice.py index b0a65b7705..db7de7b0a0 100644 --- a/pms_api_rest/datamodels/pms_invoice.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -20,3 +20,7 @@ class PmsAccountInvoiceInfo(Datamodel): 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) diff --git a/pms_api_rest/datamodels/pms_transaction.py b/pms_api_rest/datamodels/pms_transaction.py index 3d9da640d1..ee5f356a0f 100644 --- a/pms_api_rest/datamodels/pms_transaction.py +++ b/pms_api_rest/datamodels/pms_transaction.py @@ -13,7 +13,6 @@ class PmsTransactionSearchParam(Datamodel): dateEnd = fields.String(required=False, allow_none=True) transactionMethodId = fields.Integer(required=False, allow_none=True) transactionType = fields.String(required=False, allow_none=True) - # REVIEW: Fields to avoid?: class PmsTransactionsResults(Datamodel): @@ -38,6 +37,7 @@ class PmsTransactionInfo(Datamodel): 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) - isReconcilied = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index f533c9779f..72e7fad5c5 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -214,9 +214,9 @@ def get_folio_transactions(self, folio_id, pms_search_param): if payment.partner_id else None, reference=payment.ref if payment.ref else None, - isReconcilied=( - payment.reconciled_statements_count > 0 - or payment.reconciled_invoices_count > 0 + isReconcilied=(payment.reconciled_statements_count > 0), + downPaymentInvoiceId=payment.reconciled_invoice_ids.filtered( + lambda inv: inv._is_downpayment() ), ) ) @@ -606,6 +606,9 @@ def get_folio_invoices(self, folio_id): moveLines=move_lines if move_lines else None, portalUrl=portal_url, moveType=move.move_type, + isReverse=move.payment_state == "reversed", + isDownPaymentInvoice=move._is_downpayment(), + isSimpleInvoice=move.journal_id.is_simplified_invoice, ) ) return invoices diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index c065c5a02b..a43241df90 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -224,14 +224,29 @@ def get_partner_payments(self, partner_id): PmsTransactiontInfo = self.env.datamodels["pms.transaction.info"] payments = [] for payment in partnerPayments: + if payment.is_internal_transfer: + destination_journal_id = ( + payment.pms_api_counterpart_payment_id.journal_id.id + ) payments.append( PmsTransactiontInfo( id=payment.id, - amount=round(payment.amount, 2), - journalId=payment.journal_id.id, - date=payment.date.strftime("%d/%m/%Y"), - reference=payment.ref, - transactionType=payment.pms_api_transaction_type, + 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 or None, + date=datetime.combine( + payment.date, datetime.min.time() + ).isoformat(), + 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 diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 6b872816f2..3a4ac1e11c 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -179,9 +179,9 @@ def get_transactions(self, pms_transactions_search_param): if transaction.create_uid else None, transactionType=transaction.pms_api_transaction_type or None, - isReconcilied=( - transaction.reconciled_statements_count > 0 - or transaction.reconciled_invoices_count > 0 + isReconcilied=(transaction.reconciled_statements_count > 0), + downPaymentInvoiceId=transaction.reconciled_invoice_ids.filtered( + lambda inv: inv._is_downpayment() ), ) ) @@ -223,9 +223,9 @@ def get_transaction(self, transaction_id): 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 - or transaction.reconciled_invoices_count > 0 + isReconcilied=(transaction.reconciled_statements_count > 0), + downPaymentInvoiceId=transaction.reconciled_invoice_ids.filtered( + lambda inv: inv._is_downpayment() ), ) From 40f23e7eb9a1f7b0d71316a6b2b671e048842986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 16 Nov 2022 13:02:23 +0100 Subject: [PATCH 278/547] [ADD]pms_api_rest: invoice_status to folio --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index b807bec7f8..12aeae4ed4 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -38,6 +38,7 @@ class PmsFolioInfo(Datamodel): internalComment = fields.String(required=False, allow_none=True) # REVIEW: Mail workflow folio sendConfirmationMail = fields.Boolean(required=False, allow_none=True) + invoiceStatus = fields.String(required=False, allow_none=True) class PmsFolioShortInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 72e7fad5c5..e81bdbf6e2 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -51,6 +51,7 @@ def get_folio(self, folio_id): internalComment=folio.internal_comment if folio.internal_comment else None, + invoiceStatus=folio.invoice_status, ) else: raise MissingError(_("Folio not found")) From 88a0db4a3e36caf2c02040dc8bd256d1a7daaf1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 16 Nov 2022 13:03:48 +0100 Subject: [PATCH 279/547] [FIX]pms_api_rest: folio service field names --- pms_api_rest/services/pms_folio_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index e81bdbf6e2..7b062a248c 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -607,9 +607,9 @@ def get_folio_invoices(self, folio_id): moveLines=move_lines if move_lines else None, portalUrl=portal_url, moveType=move.move_type, - isReverse=move.payment_state == "reversed", + isReversed=move.payment_state == "reversed", isDownPaymentInvoice=move._is_downpayment(), - isSimpleInvoice=move.journal_id.is_simplified_invoice, + isSimplifiedInvoice=move.journal_id.is_simplified_invoice, ) ) return invoices From e4eb75646bbcc4e7ac644c60377a85d73e6b4a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 16 Nov 2022 16:48:10 +0100 Subject: [PATCH 280/547] [IMP]pms: improvement create downpayment invoice and reconcile --- pms_api_rest/datamodels/pms_invoice.py | 3 +++ pms_api_rest/services/pms_folio_service.py | 1 + pms_api_rest/services/pms_invoice_service.py | 22 ++++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/pms_api_rest/datamodels/pms_invoice.py b/pms_api_rest/datamodels/pms_invoice.py index db7de7b0a0..34cd6ab7c1 100644 --- a/pms_api_rest/datamodels/pms_invoice.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -24,3 +24,6 @@ class PmsAccountInvoiceInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 7b062a248c..3520282830 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -643,6 +643,7 @@ def create_folio_invoices(self, folio_id, invoice_info): 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 ) for item in invoice_info.saleLines: if item.id in invoices.invoice_line_ids.mapped("folio_line_ids.id"): diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index d1bffbe0d1..98610336ff 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -131,6 +131,28 @@ def _direct_move_update(self, invoice, new_vals): invoice.action_post() return invoice + @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: + if not pms_invoice_info.partnerId: + raise UserError(_("For manual invoice, partner is required")) + payment = self.env["account.payment"].browse(pms_invoice_info.paymentId) + self.env["account.payment"]._create_downpayment_invoice( + payment=payment, + partner_id=pms_invoice_info.partnerId, + ) + @restapi.method( [ ( From 331f6b064a3eb46dcb81a8d44a79860bc959c5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 16 Nov 2022 17:06:45 +0100 Subject: [PATCH 281/547] [IMP]pms_rest_api: Add isDownPayment to move line datamodel --- pms_api_rest/datamodels/pms_invoice_line.py | 1 + pms_api_rest/services/pms_folio_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_invoice_line.py b/pms_api_rest/datamodels/pms_invoice_line.py index 96532a9dbe..374fd375a4 100644 --- a/pms_api_rest/datamodels/pms_invoice_line.py +++ b/pms_api_rest/datamodels/pms_invoice_line.py @@ -13,3 +13,4 @@ class PmsInvoiceLineInfo(Datamodel): 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/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 3520282830..ac65151777 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -573,6 +573,7 @@ def get_folio_invoices(self, folio_id): saleLineId=move_line.folio_line_ids if move_line.folio_line_ids else None, + isDownPayment=move_line.move_id._is_downpayment(), ) ) move_url = ( From 2810d6d6676c30a0f0da6836addf8c4aa28ab622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 16 Nov 2022 20:50:29 +0100 Subject: [PATCH 282/547] [IMP]pms_rest_api: Improvement invoice service patch compute refund invoice --- pms_api_rest/services/pms_invoice_service.py | 114 ++++++++----------- 1 file changed, 49 insertions(+), 65 deletions(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 98610336ff..58afce2c8f 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -52,6 +52,7 @@ def update_invoice(self, invoice_id, pms_invoice_info): ) if cmd_invoice_lines: new_vals["invoice_line_ids"] = cmd_invoice_lines + new_invoice = False if new_vals: # Update Invoice # When modifying an invoice, depending on the company's configuration, @@ -60,65 +61,42 @@ def update_invoice(self, invoice_id, pms_invoice_info): # 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 != "draft": + if invoice.state == "posted": # invoice create refund - # new invoice with new_vals - move_reversal = ( - self.env["account.move.reversal"] - .with_context(active_model="account.move", active_ids=invoice.ids) - .create( - { - "date": fields.Date.today(), - "reason": _("Invoice modification"), - "refund_method": "modify", - } - ) - ) - move_reversal.reverse_moves() - reverse_invoice = move_reversal.new_move_ids - invoice = reverse_invoice - # If change invoice by reversal, and new_vals has invoice_line_ids - # we need to mapp the new invoice lines with the new invoice - reverse_lines = [] - for line in new_vals.get("invoice_line_ids", []): - origin_line = self.env["account.move.line"].browse(line[1]) - sale_line_id = origin_line.sale_line_ids.id - reverse_line = reverse_invoice.invoice_line_ids.filtered( - lambda item: item.sale_line_ids.id == sale_line_id - and item.price_unit == origin_line.price_unit - and item.quantity == origin_line.quantity - ) - if line[0] == 2: - reverse_lines.append((2, reverse_line[0].id)) - elif line[0] == 1: - reverse_lines.append((1, reverse_line[0].id, line)) + 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: - reverse_lines.append(line) - if reverse_lines: - new_vals["invoice_line_ids"] = reverse_lines + 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 + cmd_new_invoice_lines.append((item[0], new_id, item[2])) + if cmd_new_invoice_lines: + new_vals["invoice_line_ids"] = cmd_new_invoice_lines + invoice._reverse_moves(cancel=True) + # Update Journal by partner if necessary (simplified invoice -> normal invoice) new_vals["journal_id"] = ( invoice.pms_property_id._get_folio_default_journal( new_vals.get("partner_id", invoice.partner_id.id) ).id, ) - reverse_invoice.write(new_vals) - invoice = reverse_invoice - invoice.sudo().action_post() + new_invoice.write(new_vals) + new_invoice.sudo().action_post() else: - invoice = self._direct_move_update(invoice, new_vals) - # Update invoice lines name - for item in pms_invoice_info.moveLines: - if item.saleLineId in invoice.invoice_line_ids.mapped( - "folio_line_ids.id" - ): - invoice_line = invoice.invoice_line_ids.filtered( - lambda r: item.saleLineId in r.folio_line_ids.ids - ) - invoice_line.write({"name": item.name}) + new_invoice = self._direct_move_update(invoice, new_vals) + invoice_to_update = new_invoice or invoice if pms_invoice_info.narration is not None: - invoice.write({"narration": pms_invoice_info.narration}) - if invoice.state == "draft" and pms_invoice_info.state == "confirm": - invoice.action_post() + 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() return invoice.id def _direct_move_update(self, invoice, new_vals): @@ -207,10 +185,10 @@ def _get_invoice_lines_commands(self, invoice, pms_invoice_info): else: cmd_invoice_lines.append((2, line.id)) # Get the new lines to add in invoice - new_invoice_lines_info = list( + newInvoiceLinesInfo = list( filter(lambda item: not item.id, pms_invoice_info.moveLines) ) - if new_invoice_lines_info: + if newInvoiceLinesInfo: partner = ( self.env["res.partner"].browse(pms_invoice_info.partnerId) if pms_invoice_info.partnerId @@ -229,18 +207,24 @@ def _get_invoice_lines_commands(self, invoice, pms_invoice_info): } ) ) - cmd_invoice_lines.extend( - [ - item["invoice_line_ids"] - for item in folios.get_invoice_vals_list( - lines_to_invoice={ - new_invoice_lines_info[i] - .saleLineId: new_invoice_lines_info[i] - .quantity - for i in range(0, len(new_invoice_lines_info)) - }, - partner_invoice_id=partner.id, - ) + new_invoice_lines = [ + item["invoice_line_ids"] + for item in folios.get_invoice_vals_list( + lines_to_invoice={ + newInvoiceLinesInfo[i] + .saleLineId: newInvoiceLinesInfo[i] + .quantity + for i in range(0, len(newInvoiceLinesInfo)) + }, + partner_invoice_id=partner.id, + ) + ][0] + # Update name of new invoice lines + for item in 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 From bf812fd504743d072e89162c1d43871ec235c5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 17 Nov 2022 07:28:58 +0100 Subject: [PATCH 283/547] [FIX]pms_api_rest: patch invoice fix delete invoice lines --- pms_api_rest/services/pms_invoice_service.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 58afce2c8f..6232f5d66b 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -78,7 +78,12 @@ def update_invoice(self, invoice_id, pms_invoice_info): new_id = new_invoice.invoice_line_ids.filtered( lambda l: l.folio_line_ids == folio_line_ids ).id - cmd_new_invoice_lines.append((item[0], new_id, item[2])) + 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 invoice._reverse_moves(cancel=True) From 80ee86103cb97c87fbd06c8bd07c5d39040db650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 17 Nov 2022 07:30:47 +0100 Subject: [PATCH 284/547] [IMP]pms_rest_api: avoid modifying downpayme line invoice description --- pms_api_rest/services/pms_folio_service.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ac65151777..18d411d5e6 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -246,9 +246,7 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): if pms_account_payment_info.reservationIds else False ) - # TODO: no_cash_register context to maintain compatibility - # with older versions, delete it in the future - self.env["pms.folio"].with_context(no_cash_register=True).do_payment( + self.env["pms.folio"].do_payment( journal_id, journal_id.suspense_account_id, self.env.user, @@ -278,9 +276,7 @@ def create_folio_refund(self, folio_id, pms_account_payment_info): journal_id = self.env["account.journal"].browse( pms_account_payment_info.journalId ) - # TODO: no_cash_register context to maintain compatibility - # with older versions, delete it in the future - self.env["pms.folio"].with_context(no_cash_register=True).do_refund( + self.env["pms.folio"].do_refund( journal_id, journal_id.suspense_account_id, self.env.user, @@ -646,12 +642,19 @@ def create_folio_invoices(self, folio_id, invoice_info): 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 ) - invoice_line.write({"name": item.name}) + if invoice_line: + invoice_line.write({"name": item.name}) if invoice_info.narration: invoices.write({"narration": invoice_info.narration}) From 110a55e547adc8d5098e7e6813a305232cfbb7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 17 Nov 2022 10:26:41 +0100 Subject: [PATCH 285/547] [IMP]pms_api_rest: add isDownpayment to folio line datamodel --- pms_api_rest/datamodels/pms_folio_sale_line.py | 1 + pms_api_rest/services/pms_folio_service.py | 1 + pms_api_rest/services/pms_invoice_service.py | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_folio_sale_line.py b/pms_api_rest/datamodels/pms_folio_sale_line.py index 1d567716d7..1759f51f8d 100644 --- a/pms_api_rest/datamodels/pms_folio_sale_line.py +++ b/pms_api_rest/datamodels/pms_folio_sale_line.py @@ -17,3 +17,4 @@ class PmsFolioSaleInfo(Datamodel): 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/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 18d411d5e6..7b278b3ac1 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -518,6 +518,7 @@ def get_folio_sale_lines(self, folio_id): defaultInvoiceTo=sale_line.default_invoice_to if sale_line.default_invoice_to else None, + isDownPayment=sale_line.is_down_payment, ) ) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 6232f5d66b..6d6ca7aae1 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -130,7 +130,9 @@ def create_invoice(self, pms_invoice_info): if pms_invoice_info.originDownPaymentId: if not pms_invoice_info.partnerId: raise UserError(_("For manual invoice, partner is required")) - payment = self.env["account.payment"].browse(pms_invoice_info.paymentId) + 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, From 262ec89dce28d77243974269505cea2510335c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 17 Nov 2022 17:55:39 +0100 Subject: [PATCH 286/547] [FIX]pms_api_Rest: downpayment field naming --- pms_api_rest/services/pms_folio_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 7b278b3ac1..121406237a 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -518,7 +518,7 @@ def get_folio_sale_lines(self, folio_id): defaultInvoiceTo=sale_line.default_invoice_to if sale_line.default_invoice_to else None, - isDownPayment=sale_line.is_down_payment, + isDownPayment=sale_line.is_downpayment, ) ) From 4ade4453e8dd8993b9b0b0678717772d84f8ae30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 17 Nov 2022 19:00:09 +0100 Subject: [PATCH 287/547] [ADD]pms_api_rest: service booking engine send confirmation mail --- pms_api_rest/services/pms_folio_service.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 121406237a..c0f99020dd 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from odoo import _, fields -from odoo.exceptions import MissingError +from odoo.exceptions import MissingError, ValidationError from odoo.osv import expression from odoo.addons.base_rest import restapi @@ -443,7 +443,24 @@ def create_folio(self, pms_folio_info): ], } self.env["pms.service"].create(vals) - + # 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) return folio.id @restapi.method( From e5d6bbc920744516b6c233679d074d900d18eebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 17 Nov 2022 21:48:46 +0100 Subject: [PATCH 288/547] [WIP]pms_api_rest: Add end points to reports (with report example return -transactions-) --- .../services/pms_reservation_service.py | 98 ++++++++++++++++++- pms_api_rest/services/pms_service_service.py | 34 ++++++- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 65be7c7be1..cfae9a42b6 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -1,7 +1,7 @@ import base64 from datetime import datetime, timedelta -from odoo import _ +from odoo import _, fields from odoo.exceptions import MissingError from odoo.addons.base_rest import restapi @@ -776,3 +776,99 @@ def print_checkin(self, reservation_id, checkin_partner_id): 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): + # TODO: Implement kelly report + 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["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) + + @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): + # TODO: Implment arrivals report + 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["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) + + @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): + # TODO: Implement departures report + 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["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) diff --git a/pms_api_rest/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py index 5b27914383..76018fcd57 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -1,7 +1,7 @@ import logging from datetime import datetime -from odoo import _ +from odoo import _, fields from odoo.exceptions import MissingError from odoo.addons.base_rest import restapi @@ -171,3 +171,35 @@ def get_service_lines(self, service_id): ) ) 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): + # TODO: Implment arrivals report + 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["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) From 95140e6c4cf8d8f608637d0460348d27d08a08e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 19 Nov 2022 12:52:28 +0100 Subject: [PATCH 289/547] [ADD]pms_api_rest: basic reports services --- pms/models/pms_reservation.py | 12 +- pms_api_rest/__manifest__.py | 2 + pms_api_rest/data/sql_reports.xml | 130 ++++++++++++++++++ pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/sql_export.py | 15 ++ .../services/pms_reservation_service.py | 66 +++++---- pms_api_rest/services/pms_service_service.py | 27 ++-- 7 files changed, 206 insertions(+), 47 deletions(-) create mode 100644 pms_api_rest/data/sql_reports.xml create mode 100644 pms_api_rest/models/sql_export.py diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 185a4f34d0..50769fe358 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -997,12 +997,12 @@ def _compute_service_ids(self): ] ) # Avoid recalculating services if the boardservice has not changed - if ( - old_board_lines - and reservation.board_service_room_id - == reservation._origin.board_service_room_id - ): - return + # if ( + # old_board_lines + # and reservation.board_service_room_id + # == reservation._origin.board_service_room_id + # ): + # return if reservation.board_service_room_id: board = self.env["pms.board.service.room.type"].browse( reservation.board_service_room_id.id diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index c55bf76834..e884b0fbe1 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -14,11 +14,13 @@ "auth_jwt_login", "base_location", "l10n_es_aeat", + "sql_export_excel", ], "external_dependencies": { "python": ["jwt", "simplejson", "marshmallow", "jose"], }, "data": [ + "data/sql_reports.xml", "data/auth_jwt_validator.xml", "views/pms_property_views.xml", "views/res_users_views.xml", diff --git a/pms_api_rest/data/sql_reports.xml b/pms_api_rest/data/sql_reports.xml new file mode 100644 index 0000000000..3e9626ec9d --- /dev/null +++ b/pms_api_rest/data/sql_reports.xml @@ -0,0 +1,130 @@ + + + + x_date_from + 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" + 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.pms_property_id = %(x_pms_property_id)s) + GROUP BY + line.id, product_tmpl.name, reservation.name, reservation.rooms, reservation.adults, reservation.children; + + + + + + diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 495168b213..4713a4d6b7 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -1,3 +1,4 @@ from . import pms_property from . import res_users from . import account_payment +from . import sql_export 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/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index cfae9a42b6..52e6addc26 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -791,19 +791,17 @@ def print_checkin(self, reservation_id, checkin_partner_id): auth="jwt_api_pms", ) def kelly_report(self, pms_report_search_param): - # TODO: Implement kelly report 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["cash.daily.report.wizard"].create( + report_wizard = self.env["kellysreport"].create( { "date_start": date_from, - "date_end": date_to, "pms_property_id": pms_property_id, } ) - result = report_wizard._export() + report_wizard.calculate_report() + result = report_wizard._excel_export() file_name = result["xls_filename"] base64EncodedStr = result["xls_binary"] PmsResponse = self.env.datamodels["pms.report"] @@ -823,21 +821,25 @@ def kelly_report(self, pms_report_search_param): auth="jwt_api_pms", ) def arrivals_report(self, pms_report_search_param): - # TODO: Implment arrivals report 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["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"] + 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) @@ -855,20 +857,26 @@ def arrivals_report(self, pms_report_search_param): auth="jwt_api_pms", ) def departures_report(self, pms_report_search_param): - # TODO: Implement departures report 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["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"] + 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) diff --git a/pms_api_rest/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py index 76018fcd57..4c93c1de60 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -186,20 +186,23 @@ def get_service_lines(self, service_id): auth="jwt_api_pms", ) def services_report(self, pms_report_search_param): - # TODO: Implment arrivals report 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["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"] + 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_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) From b6f206bb1a0b5e97b557368164af1607d21b3ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 19 Nov 2022 18:14:12 +0100 Subject: [PATCH 290/547] [RFC]pms_api_rest: refactoring PATCH services --- pms_api_rest/services/pms_calendar_service.py | 8 ++++---- pms_api_rest/services/pms_folio_service.py | 9 +++++++-- pms_api_rest/services/pms_invoice_service.py | 6 +++--- pms_api_rest/services/pms_partner_service.py | 2 +- .../services/pms_reservation_line_service.py | 6 +++--- .../services/pms_reservation_service.py | 17 ++++++++--------- pms_api_rest/services/pms_room_service.py | 11 ++++++++--- .../services/pms_service_line_service.py | 6 +++--- .../services/pms_transaction_service.py | 6 +++--- 9 files changed, 40 insertions(+), 31 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index f4476dd489..dda14d1ec9 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -504,21 +504,21 @@ def update_reservation(self, reservation_id, reservation_lines_changes): reservation_vals.update( {"preferred_room_id": reservation_lines_changes.preferredRoomId} ) - if reservation_lines_changes.boardServiceId: + if reservation_lines_changes.boardServiceId is not None: reservation_vals.update( {"board_service_room_id": reservation_lines_changes.boardServiceId} ) - if reservation_lines_changes.pricelistId: + if reservation_lines_changes.pricelistId is not None: 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: + if reservation_lines_changes.children is not None: reservation_vals.update( {"children": reservation_lines_changes.children} ) - if reservation_lines_changes.segmentationId: + if reservation_lines_changes.segmentationId is not None: reservation_vals.update( { "segmentation_ids": [ diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index c0f99020dd..2f74a16bcf 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -477,11 +477,16 @@ def create_folio(self, pms_folio_info): ) def update_folio(self, folio_id, pms_folio_info): folio = self.env["pms.folio"].browse(folio_id) + folio_vals = {} if folio: - folio.write({"internal_comment": pms_folio_info.internalComment}) - else: raise MissingError(_("Folio not found")) + if pms_folio_info.internalComment is not None: + folio_vals["internal_comment"]: pms_folio_info.internalComment + + if folio_vals: + folio.write(folio_vals) + @restapi.method( [ ( diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 6d6ca7aae1..e493f5c837 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -33,12 +33,12 @@ def update_invoice(self, invoice_id, pms_invoice_info): raise UserError(_("You can't update a reversed invoice")) new_vals = {} if ( - pms_invoice_info.partnerId + pms_invoice_info.partnerId is not None and pms_invoice_info.partnerId != invoice.partner_id.id ): new_vals["partner_id"] = pms_invoice_info.partnerId - if pms_invoice_info.date: + if pms_invoice_info.date is not None: invoice_date_info = fields.Date.from_string(pms_invoice_info.date) if invoice_date_info != invoice.invoice_date: new_vals["invoice_date"] = invoice_date_info @@ -46,7 +46,7 @@ def update_invoice(self, invoice_id, pms_invoice_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 and pms_invoice_info.moveLines is not None: + if pms_invoice_info.moveLines is not None: cmd_invoice_lines = self._get_invoice_lines_commands( invoice, pms_invoice_info ) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index a43241df90..453db7d540 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -568,6 +568,6 @@ def mapping_partner_values(self, pms_partner_info): birthdate = birthdate.strftime("%Y-%m-%d") vals.update({"birthdate_date": birthdate}) for k, v in partner_fields.items(): - if v: + if v is not None: vals.update({k: v}) return vals diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index fee38b3030..2ea1695bde 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -118,11 +118,11 @@ def update_reservation_line(self, reservation_line_id, reservation_line_info): ) vals = dict() if reservation_line: - if reservation_line_info.price: + if reservation_line_info.price is not None: vals["price"] = reservation_line_info.price - if reservation_line_info.discount: + if reservation_line_info.discount is not None: vals["discount"] = reservation_line_info.discount - if reservation_line_info.cancelDiscount: + 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 diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 52e6addc26..5dcfd8dc76 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -122,15 +122,15 @@ def _create_vals_from_params(self, reservation_vals, reservation_data): reservation_vals.update( {"preferred_room_id": reservation_data.preferredRoomId} ) - if reservation_data.boardServiceId: + if reservation_data.boardServiceId is not None: reservation_vals.update( - {"board_service_room_id": reservation_data.boardServiceId} + {"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: + if reservation_data.children is not None: reservation_vals.update({"children": reservation_data.children}) if reservation_data.segmentationId: reservation_vals.update( @@ -554,17 +554,16 @@ def write_reservation_checkin_partner( 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 - and pms_checkin_partner_info.actionOnBoard - and checkin_partner ): checkin_partner.action_on_board() - if checkin_partner: - checkin_partner.write( - self.mapping_checkin_partner_values(pms_checkin_partner_info) - ) + checkin_partner.write( + self.mapping_checkin_partner_values(pms_checkin_partner_info) + ) return checkin_partner.id @restapi.method( diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 1132133f6d..4bd253d0af 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -136,11 +136,16 @@ def get_room(self, room_id): ) def update_room(self, room_id, pms_room_info_data): room = self.env["pms.room"].search([("id", "=", room_id)]) - if room: - room.name = pms_room_info_data.name - else: + 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( [ ( diff --git a/pms_api_rest/services/pms_service_line_service.py b/pms_api_rest/services/pms_service_line_service.py index b34de7baf0..0cb90cc251 100644 --- a/pms_api_rest/services/pms_service_line_service.py +++ b/pms_api_rest/services/pms_service_line_service.py @@ -64,11 +64,11 @@ def update_service_line(self, service_line_id, pms_service_line_info_data): vals["date"] = datetime.strptime( pms_service_line_info_data.date, "%Y-%m-%d" ).date() - if pms_service_line_info_data.discount: + 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: + 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: + if pms_service_line_info_data.priceUnit is not None: vals["price_unit"] = pms_service_line_info_data.priceUnit service_line.write(vals) else: diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 3a4ac1e11c..4b4cf1a5df 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -311,17 +311,17 @@ def update_transaction(self, transaction_id, pms_transaction_info): counterpart_transaction = False # TODO: Downpayment invoiced (search invoice, reverse it and create a new one) # Get generic update vals - if pms_transaction_info.amount and round( + 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 if ( - pms_transaction_info.partnerId + pms_transaction_info.partnerId is not None and pms_transaction_info.partnerId != transaction.partner_id.id ): vals["partner_id"] = pms_transaction_info.partnerId if ( - pms_transaction_info.reference + pms_transaction_info.reference is not None and pms_transaction_info.reference != transaction.ref ): vals["ref"] = pms_transaction_info.reference From 624a4ffed67856bc0638af77f6aae0d4aa5cb64d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 20 Nov 2022 10:15:27 +0100 Subject: [PATCH 291/547] [ADD]pms_api_rest: pricelist pms enabled configuration --- pms_api_rest/services/pms_pricelist_service.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 21061cdd1c..3fa38af657 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -28,7 +28,11 @@ class PmsPricelistService(Component): auth="jwt_api_pms", ) def get_pricelists(self, pms_search_param, **args): - pricelists = self.env["product.pricelist"].search([]) + pricelists = self.env["product.pricelist"].search( + [ + ("is_pms_available", "=", True), + ] + ) if pms_search_param.pmsPropertyIds and pms_search_param.pmsPropertyId: raise ValidationError( _( From 28f1d15671ba7f8514fe3597363bcd0b7441a95a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 20 Nov 2022 10:16:00 +0100 Subject: [PATCH 292/547] [TMP]pms_api_rest: review input params None/False/'' --- pms_api_rest/services/pms_calendar_service.py | 4 ++-- pms_api_rest/services/pms_invoice_service.py | 4 ++-- pms_api_rest/services/pms_transaction_service.py | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index dda14d1ec9..16bc4aa243 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -508,7 +508,7 @@ def update_reservation(self, reservation_id, reservation_lines_changes): reservation_vals.update( {"board_service_room_id": reservation_lines_changes.boardServiceId} ) - if reservation_lines_changes.pricelistId is not None: + if reservation_lines_changes.pricelistId: reservation_vals.update( {"pricelist_id": reservation_lines_changes.pricelistId} ) @@ -518,7 +518,7 @@ def update_reservation(self, reservation_id, reservation_lines_changes): reservation_vals.update( {"children": reservation_lines_changes.children} ) - if reservation_lines_changes.segmentationId is not None: + if reservation_lines_changes.segmentationId: reservation_vals.update( { "segmentation_ids": [ diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index e493f5c837..7531a8f772 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -33,12 +33,12 @@ def update_invoice(self, invoice_id, pms_invoice_info): raise UserError(_("You can't update a reversed invoice")) new_vals = {} if ( - pms_invoice_info.partnerId is not None + 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 is not None: + if pms_invoice_info.date: invoice_date_info = fields.Date.from_string(pms_invoice_info.date) if invoice_date_info != invoice.invoice_date: new_vals["invoice_date"] = invoice_date_info diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 4b4cf1a5df..473ddcc081 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -315,8 +315,10 @@ def update_transaction(self, transaction_id, pms_transaction_info): 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 is not None + pms_transaction_info.partnerId and pms_transaction_info.partnerId != transaction.partner_id.id ): vals["partner_id"] = pms_transaction_info.partnerId From f988d8cb8c5963e84e876af255aa0111d5612ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 20 Nov 2022 10:29:42 +0100 Subject: [PATCH 293/547] [ADD]pms_api_rest: product pms enabled configuration --- pms_api_rest/services/pms_product_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py index a45176e6ac..c550c799ca 100644 --- a/pms_api_rest/services/pms_product_service.py +++ b/pms_api_rest/services/pms_product_service.py @@ -26,7 +26,7 @@ class PmsProductService(Component): auth="jwt_api_pms", ) def get_products(self, product_search_param): - domain = [("sale_ok", "=", True)] + domain = [("sale_ok", "=", True), ("is_pms_available", "=", True)] if product_search_param.name: domain.append(("name", "like", product_search_param.name)) if product_search_param.pmsPropertyId: From cc1e37a440431c32f0151b98271066056f8e6c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 20 Nov 2022 17:18:45 +0100 Subject: [PATCH 294/547] [FIX]pms: fix again recompute boards to avoid importer error --- pms/models/pms_reservation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 50769fe358..185a4f34d0 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -997,12 +997,12 @@ def _compute_service_ids(self): ] ) # Avoid recalculating services if the boardservice has not changed - # if ( - # old_board_lines - # and reservation.board_service_room_id - # == reservation._origin.board_service_room_id - # ): - # return + if ( + old_board_lines + and reservation.board_service_room_id + == reservation._origin.board_service_room_id + ): + return if reservation.board_service_room_id: board = self.env["pms.board.service.room.type"].browse( reservation.board_service_room_id.id From 5f0dc677bb50067aa7dc767ef5cb6bd4f09311a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 21 Nov 2022 20:34:09 +0100 Subject: [PATCH 295/547] [FIX]pms_api_rest: force to write invoice lines name when a invoice is updated --- pms_api_rest/services/pms_invoice_service.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 7531a8f772..35167fa15a 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -109,7 +109,25 @@ def _direct_move_update(self, invoice, new_vals): if previus_state == "posted": invoice.button_draft() if new_vals: + # 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. + + # 1- save send invoice line name values: + 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] + } + # 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]} + ) if previus_state == "posted": invoice.action_post() return invoice @@ -231,7 +249,7 @@ def _get_invoice_lines_commands(self, invoice, pms_invoice_info): item[2]["name"] = [ line.name for line in newInvoiceLinesInfo - if line.saleLineId == item[2]["folio_line_ids"][0][2] + if [line.saleLineId] == item[2]["folio_line_ids"][0][2] ][0] cmd_invoice_lines.extend(new_invoice_lines) return cmd_invoice_lines From d283de167d69fcd07a15201dbc3506e185b7137e Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 22 Nov 2022 13:29:27 +0100 Subject: [PATCH 296/547] [REF]pms_api_rest: added action_cancel and confirm in folio PATCH --- pms_api_rest/datamodels/pms_folio.py | 2 ++ pms_api_rest/services/pms_folio_service.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 12aeae4ed4..7356306694 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -38,6 +38,8 @@ class PmsFolioInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 2f74a16bcf..62a3f384ee 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -478,9 +478,13 @@ def create_folio(self, pms_folio_info): def update_folio(self, folio_id, pms_folio_info): folio = self.env["pms.folio"].browse(folio_id) folio_vals = {} - if folio: + 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.confirm() if pms_folio_info.internalComment is not None: folio_vals["internal_comment"]: pms_folio_info.internalComment From 5b50cf60d189a9ff8da3375679262b37e9f0f7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 22 Nov 2022 21:49:21 +0100 Subject: [PATCH 297/547] [IMP]pms_api_rest: add qty_to_invoice in section sale lines, and auto-open cash session --- pms_api_rest/services/pms_folio_service.py | 113 ++++++++++++++++-- .../services/pms_transaction_service.py | 28 +++++ 2 files changed, 128 insertions(+), 13 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 62a3f384ee..f0598e703c 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -3,6 +3,7 @@ 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 @@ -238,17 +239,27 @@ def get_folio_transactions(self, folio_id, pms_search_param): 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_id = self.env["account.journal"].browse( - pms_account_payment_info.journalId - ) + 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_id, - journal_id.suspense_account_id, + journal, + journal.suspense_account_id, self.env.user, pms_account_payment_info.amount, folio, @@ -273,12 +284,22 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): 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_id = self.env["account.journal"].browse( - pms_account_payment_info.journalId - ) + 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_id, - journal_id.suspense_account_id, + journal, + journal.suspense_account_id, self.env.user, pms_account_payment_info.amount, folio, @@ -519,9 +540,9 @@ def get_folio_sale_lines(self, folio_id): priceUnit=sale_line.price_unit if sale_line.price_unit else None, - qtyToInvoice=sale_line.qty_to_invoice - if sale_line.qty_to_invoice - 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, @@ -686,3 +707,69 @@ def create_folio_invoices(self, folio_id, invoice_info): 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 diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 473ddcc081..2d93af0527 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -273,8 +273,36 @@ def create_transaction(self, pms_transaction_info): .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, From a685d971a9761f24e5a68fa3565faba766a884b9 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 23 Nov 2022 16:37:10 +0100 Subject: [PATCH 298/547] [ADD] pms_api_rest: add reservation id to reservation service service --- pms_api_rest/datamodels/pms_service.py | 1 + pms_api_rest/services/pms_reservation_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py index 49ce06f78c..ef0e662132 100644 --- a/pms_api_rest/datamodels/pms_service.py +++ b/pms_api_rest/datamodels/pms_service.py @@ -7,6 +7,7 @@ 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) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 5dcfd8dc76..9a38c4926b 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -397,6 +397,7 @@ def get_reservation_services(self, reservation_id): result_services.append( PmsServiceInfo( id=service.id, + reservationId=service.reservation_id, name=service.name, productId=service.product_id.id, quantity=service.product_qty, From 5b0b1d187b0e9408cff36e435ef0dddf550a13ac Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 23 Nov 2022 16:39:15 +0100 Subject: [PATCH 299/547] [FIX] pms-api-rest: agency id @ folio datamodel not required --- pms_api_rest/datamodels/pms_folio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 7356306694..2931ec22c7 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -31,7 +31,7 @@ class PmsFolioInfo(Datamodel): ) pricelistId = fields.Integer(required=False, allow_none=False) saleChannelId = fields.Integer(required=False, allow_none=False) - agencyId = fields.Integer(required=False, allow_none=False) + agencyId = fields.Integer(required=False, allow_none=True) externalReference = fields.String(required=False, allow_none=True) closureReasonId = fields.Integer(required=False, allow_none=True) preconfirm = fields.Boolean(required=False, allow_none=True) From fc3513a40330cfa7dbef22fb45bd2542ba041144 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 23 Nov 2022 16:40:02 +0100 Subject: [PATCH 300/547] [FIX] pms-api-rest: update folio @ folio service to add reservations --- pms_api_rest/services/pms_folio_service.py | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index f0598e703c..c905c0c68e 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -53,6 +53,14 @@ def get_folio(self, folio_id): 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, ) else: raise MissingError(_("Folio not found")) @@ -508,6 +516,43 @@ def update_folio(self, folio_id, pms_folio_info): reservation.confirm() if pms_folio_info.internalComment is not None: folio_vals["internal_comment"]: pms_folio_info.internalComment + 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) From 5a471845f1db4e8c6fcb5f08d89f86fcc4ac785e Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 23 Nov 2022 17:53:44 +0100 Subject: [PATCH 301/547] [FIX] pms-api-rest: fix vals & condition to create reservations @ update folio --- pms_api_rest/services/pms_folio_service.py | 78 +++++++++++----------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index c905c0c68e..b60406b1a1 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -515,45 +515,45 @@ def update_folio(self, folio_id, pms_folio_info): for reservation in folio.reservation_ids: reservation.confirm() if pms_folio_info.internalComment is not None: - folio_vals["internal_comment"]: pms_folio_info.internalComment - 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) - + folio_vals.update({"internal_comment": pms_folio_info.internalComment}) + 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) From be9d0b7fbc82c75a284792f629019a8ad78d854a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 23 Nov 2022 20:59:12 +0100 Subject: [PATCH 302/547] [IMP]pms_rest_api: Improvement folio search --- pms_api_rest/services/pms_folio_service.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index b60406b1a1..0a08255ebe 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -103,14 +103,15 @@ def get_folios(self, folio_search_param): domain_filter = list() if folio_search_param.filter: - for search in folio_search_param.filter.split(" "): + target = folio_search_param.filter + if "@" in target: + domain_filter.append(("email", "ilike", target)) + else: subdomains = [ - [("name", "ilike", search)], - [("folio_id.name", "ilike", search)], - [("partner_name", "ilike", search)], - [("partner_id.firstname", "ilike", search)], - [("partner_id.lastname", "ilike", search)], - [("partner_id.id_numbers.name", "ilike", search)], + [("name", "ilike", target)], + [("partner_name", "ilike", "%".join(target.split(" ")))], + [("mobile", "ilike", target)], + [("external_reference", "ilike", target)], ] domain_filter.append(expression.OR(subdomains)) domain = [] @@ -126,7 +127,7 @@ def get_folios(self, folio_search_param): PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] for folio in self.env["pms.folio"].search( - [("id", "in", reservations_result)], + [("id", "in", reservations_result)], order="write_date desc" ): reservations = [] for reservation in folio.reservation_ids: From b503e689b7df8490f6730cebb71e6a044d99bf11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 23 Nov 2022 20:59:42 +0100 Subject: [PATCH 303/547] [FIX]pms_api_rest: fix patch discount to 0 --- pms_api_rest/services/pms_reservation_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 9a38c4926b..bed800bf05 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -217,11 +217,11 @@ def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): not reservation_line or origin_data.price != reservation_line.price ): line_vals["price"] = origin_data.price - if origin_data.discount and ( + if origin_data.discount is not None and ( not reservation_line or origin_data.discount != reservation_line.discount ): line_vals["discount"] = origin_data.discount - if origin_data.cancelDiscount and ( + if origin_data.cancelDiscount is not None and ( not reservation_line or origin_data.cancelDiscount != reservation_line.cancelDiscount ): From c682d9326c4d2423d84b05f16117eeed0b3305bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 23 Nov 2022 22:32:21 +0100 Subject: [PATCH 304/547] [FIX]pms_api_rest: get_reservations partner_name or none --- pms_api_rest/services/pms_reservation_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index bed800bf05..5389af2026 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -61,7 +61,7 @@ def get_reservation(self, reservation_id, pms_search_param): name=reservation.name, folioId=reservation.folio_id.id, folioSequence=reservation.folio_sequence, - partnerName=reservation.partner_name, + partnerName=reservation.partner_name or None, boardServiceId=reservation.board_service_room_id.id if reservation.board_service_room_id else None, From b86793b915b6039c53bb4ac5267554ee83f164ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 24 Nov 2022 11:03:10 +0100 Subject: [PATCH 305/547] [FIX]pms_api_rest: allow none datamodel folio fields (staff and outservices) --- pms_api_rest/datamodels/pms_folio.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 2931ec22c7..75375556b5 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -24,13 +24,13 @@ class PmsFolioInfo(Datamodel): reservationType = fields.String(required=False, allow_none=True) pendingAmount = fields.Float(required=False, allow_none=True) lastCheckout = fields.String(required=False, allow_none=True) - pmsPropertyId = fields.Integer(required=False, allow_none=False) - partnerId = fields.Integer(required=False, allow_none=False) + 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=False + NestedModel("pms.reservation.info"), required=False, allow_none=True ) - pricelistId = fields.Integer(required=False, allow_none=False) - saleChannelId = fields.Integer(required=False, allow_none=False) + 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) From fcf624200a46553195a4e4331bbc0686ca60f2d1 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 25 Nov 2022 20:40:00 +0100 Subject: [PATCH 306/547] [REF]pms_api_rest: unlink checkin partner --- pms_api_rest/services/pms_reservation_service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 5389af2026..25af0fd395 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -681,8 +681,9 @@ def create_reservation_checkin_partner( auth="jwt_api_pms", ) def delete_reservation_checkin_partner(self, reservation_id, checkin_partner_id): - reservation = self.env["pms.reservation"].browse(reservation_id) - reservation.adults = reservation.adults - 1 + 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): vals = dict() From cee0b87f002f90c4105491257c248e2d9f234cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 23 Nov 2022 16:54:18 +0100 Subject: [PATCH 307/547] [ADD]pms_api_rest: Basic notifications service --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/services/__init__.py | 1 + .../services/pms_notification_service.py | 70 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 pms_api_rest/services/pms_notification_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 84fd467111..93946d74ef 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -56,3 +56,4 @@ from . import pms_folio_sale_line from . import pms_invoice_line from . import pms_mail +from . import pms_notification diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index cab1415013..bf7d290a4f 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -39,3 +39,4 @@ from . import pms_account_payment_terms_service from . import pms_account_journal_service from . import pms_invoice_service +from . import pms_notification_service 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..2ef40ae392 --- /dev/null +++ b/pms_api_rest/services/pms_notification_service.py @@ -0,0 +1,70 @@ +from datetime import datetime + +import pytz + +from odoo import _ + +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( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.notification.search", is_list=False), + output_param=Datamodel("pms.notification.info", is_list=True), + auth="jwt_api_pms", + cors="*", + ) + def get_notifications(self, pms_notification_search): + from_datetime = datetime.strptime( + pms_notification_search.fromDateTime, "%Y-%m-%d %H:%M:%S" + ) + timezone = pytz.timezone(self.env.user.tz or "UTC") + from_datetime = timezone.localize(from_datetime) + from_datetime_utc = from_datetime.astimezone(pytz.utc) + new_reservations = self.env["pms.reservation"].search( + [ + ("create_date", ">=", from_datetime_utc), + ("pms_property_id.id", "=", pms_notification_search.pmsPropertyId), + ("to_assign", "=", True), + ("create_uid.id", "!=", self.env.user.id), + ], + limit=10, + order="create_date desc", + ) + notifications = [] + PmsNotificationInfo = self.env.datamodels["pms.notification.info"] + for folio in new_reservations.mapped("folio_id"): + notifications.append( + PmsNotificationInfo( + pmsPropertyId=folio.pms_property_id.id, + folioId=folio.id, + dateTime=pytz.UTC.localize(folio.create_date) + .astimezone(timezone) + .strftime("%Y-%m-%d %H:%M:%S"), + userId=folio.create_uid.id, + mensaje=_("%s: Nueva reserva de %s por %s") + % ( + folio.name, + folio.partner_name, + folio.agency_id.name + if folio.agency_id + else folio.sale_channel_origin_id.name, + ), + ) + ) + return notifications From 42c56cf6efe802d78c8709493e83042139e7e6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 23 Nov 2022 20:32:13 +0100 Subject: [PATCH 308/547] [ADD]pms_rest_api: add datamodel notifications --- pms_api_rest/datamodels/pms_notification.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_notification.py diff --git a/pms_api_rest/datamodels/pms_notification.py b/pms_api_rest/datamodels/pms_notification.py new file mode 100644 index 0000000000..acebfc17c3 --- /dev/null +++ b/pms_api_rest/datamodels/pms_notification.py @@ -0,0 +1,18 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsNotificationSearch(Datamodel): + _name = "pms.notification.search" + pmsPropertyId = fields.Integer(required=False) + fromDateTime = fields.String(required=False) + + +class PmsNotificationInfo(Datamodel): + _name = "pms.notification.info" + folioId = fields.Integer(required=False) + dateTime = fields.String(required=False) + userId = fields.Integer(required=False) + mensaje = fields.String(required=False) + pmsPropertyId = fields.Integer(required=False) From ae14b3541bdfe4521e095c822b2669992c18b9ec Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 25 Nov 2022 21:28:58 +0100 Subject: [PATCH 309/547] [FIX] pms-api-rest: change param timestamp --- pms_api_rest/datamodels/pms_notification.py | 12 +++--- .../services/pms_notification_service.py | 40 +++++++------------ 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/pms_api_rest/datamodels/pms_notification.py b/pms_api_rest/datamodels/pms_notification.py index acebfc17c3..2d7e74e110 100644 --- a/pms_api_rest/datamodels/pms_notification.py +++ b/pms_api_rest/datamodels/pms_notification.py @@ -5,14 +5,14 @@ class PmsNotificationSearch(Datamodel): _name = "pms.notification.search" - pmsPropertyId = fields.Integer(required=False) - fromDateTime = fields.String(required=False) + fromTimestamp = fields.String(required=False) class PmsNotificationInfo(Datamodel): _name = "pms.notification.info" - folioId = fields.Integer(required=False) - dateTime = fields.String(required=False) - userId = fields.Integer(required=False) - mensaje = fields.String(required=False) 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) diff --git a/pms_api_rest/services/pms_notification_service.py b/pms_api_rest/services/pms_notification_service.py index 2ef40ae392..9680853a3d 100644 --- a/pms_api_rest/services/pms_notification_service.py +++ b/pms_api_rest/services/pms_notification_service.py @@ -1,8 +1,4 @@ -from datetime import datetime - -import pytz - -from odoo import _ +import datetime from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -30,18 +26,18 @@ class PmsNotificationService(Component): cors="*", ) def get_notifications(self, pms_notification_search): - from_datetime = datetime.strptime( - pms_notification_search.fromDateTime, "%Y-%m-%d %H:%M:%S" + from_date_time = datetime.datetime.fromtimestamp( + int(pms_notification_search.fromTimestamp) / 1000 ) - timezone = pytz.timezone(self.env.user.tz or "UTC") - from_datetime = timezone.localize(from_datetime) - from_datetime_utc = from_datetime.astimezone(pytz.utc) new_reservations = self.env["pms.reservation"].search( [ - ("create_date", ">=", from_datetime_utc), - ("pms_property_id.id", "=", pms_notification_search.pmsPropertyId), + ("create_date", ">=", from_date_time), ("to_assign", "=", True), - ("create_uid.id", "!=", self.env.user.id), + ( + "create_uid.id", + "!=", + self.env.user.id, + ), ], limit=10, order="create_date desc", @@ -53,18 +49,12 @@ def get_notifications(self, pms_notification_search): PmsNotificationInfo( pmsPropertyId=folio.pms_property_id.id, folioId=folio.id, - dateTime=pytz.UTC.localize(folio.create_date) - .astimezone(timezone) - .strftime("%Y-%m-%d %H:%M:%S"), - userId=folio.create_uid.id, - mensaje=_("%s: Nueva reserva de %s por %s") - % ( - folio.name, - folio.partner_name, - folio.agency_id.name - if folio.agency_id - else folio.sale_channel_origin_id.name, - ), + timeStamp=int(folio.create_date.strftime("%s%f")) / 1000, + folioName=folio.name, + partnerName=folio.partner_name, + saleChannelName=folio.agency_id.name + if folio.agency_id + else folio.sale_channel_origin_id.name, ) ) return notifications From 24cc145494d48941ab82604b7e8f04dabd025b27 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 25 Nov 2022 21:49:05 +0100 Subject: [PATCH 310/547] [FIX] fix when no sale channel name --- pms_api_rest/datamodels/pms_notification.py | 2 +- pms_api_rest/services/pms_notification_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_notification.py b/pms_api_rest/datamodels/pms_notification.py index 2d7e74e110..9582c219c2 100644 --- a/pms_api_rest/datamodels/pms_notification.py +++ b/pms_api_rest/datamodels/pms_notification.py @@ -15,4 +15,4 @@ class PmsNotificationInfo(Datamodel): timeStamp = fields.Integer(required=False) folioName = fields.String(required=False) partnerName = fields.String(required=False) - saleChannelName = fields.String(required=False) + saleChannelName = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_notification_service.py b/pms_api_rest/services/pms_notification_service.py index 9680853a3d..81af342deb 100644 --- a/pms_api_rest/services/pms_notification_service.py +++ b/pms_api_rest/services/pms_notification_service.py @@ -54,7 +54,7 @@ def get_notifications(self, pms_notification_search): partnerName=folio.partner_name, saleChannelName=folio.agency_id.name if folio.agency_id - else folio.sale_channel_origin_id.name, + else folio.sale_channel_origin_id.name or None, ) ) return notifications From 704e4f99e5b1e3ebaf3494d239e846f005486d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 27 Nov 2022 09:56:33 +0100 Subject: [PATCH 311/547] [IMP]pms_api_rest: improvement order folio reservations slide by sequence --- pms_api_rest/services/pms_folio_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 0a08255ebe..ca9ae6e8b1 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -339,7 +339,9 @@ def get_folio_reservations(self, folio_id): pass else: if folio.reservation_ids: - for reservation in folio.reservation_ids: + for reservation in sorted( + folio.reservation_ids, key=lambda r: r.folio_sequence + ): reservations.append( PmsReservationShortInfo( id=reservation.id, From 32ca815a0a5747b4df891274548332d0694ce543 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 28 Nov 2022 13:49:24 +0100 Subject: [PATCH 312/547] [FIX] pms-api-rest: remove services when no board service @ udpate reservation service --- pms_api_rest/services/pms_reservation_service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 25af0fd395..d1cad6148f 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -206,9 +206,12 @@ def update_reservation(self, reservation_id, reservation_data): reservation_vals = self._create_vals_from_params( reservation_vals, reservation_data ) + if reservation_data.boardServiceId == 0: + reservation.service_ids.filtered(lambda x: x.is_board_service).unlink() if reservation_vals: reservation.write(reservation_vals) + 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) From b7215220239a35a658bf30c2d26a36fa50f62fae Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 28 Nov 2022 19:54:23 +0100 Subject: [PATCH 313/547] [IMP] pms-api-rest: add taxes percentage to prdouct service --- pms_api_rest/datamodels/pms_product.py | 1 + pms_api_rest/services/pms_product_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_product.py b/pms_api_rest/datamodels/pms_product.py index 9a11a0bd2d..22a0c85bfa 100644 --- a/pms_api_rest/datamodels/pms_product.py +++ b/pms_api_rest/datamodels/pms_product.py @@ -16,3 +16,4 @@ class PmProductInfo(Datamodel): perDay = fields.Boolean(required=False, allow_none=True) perPerson = fields.Boolean(required=False, allow_none=True) consumedOn = fields.String(required=False, allow_none=True) + taxesPercentage = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py index c550c799ca..eaa70b20ac 100644 --- a/pms_api_rest/services/pms_product_service.py +++ b/pms_api_rest/services/pms_product_service.py @@ -53,6 +53,7 @@ def get_products(self, product_search_param): perDay=product.per_day, perPerson=product.per_person, consumedOn=product.consumed_on, + taxesPercentage=product.taxes_id.amount if product.taxes_id.amount_type == 'percent' else None, ) ) return result_products From 4fccad781a6c387cfcc5d29fdd9f11db620bcda5 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 29 Nov 2022 16:56:02 +0100 Subject: [PATCH 314/547] [FIX] pms-api-rest: remove taxes percentage @ product service & reload services when board service chagned --- pms_api_rest/datamodels/pms_product.py | 1 - pms_api_rest/services/pms_product_service.py | 1 - pms_api_rest/services/pms_reservation_service.py | 3 ++- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/datamodels/pms_product.py b/pms_api_rest/datamodels/pms_product.py index 22a0c85bfa..9a11a0bd2d 100644 --- a/pms_api_rest/datamodels/pms_product.py +++ b/pms_api_rest/datamodels/pms_product.py @@ -16,4 +16,3 @@ class PmProductInfo(Datamodel): perDay = fields.Boolean(required=False, allow_none=True) perPerson = fields.Boolean(required=False, allow_none=True) consumedOn = fields.String(required=False, allow_none=True) - taxesPercentage = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py index eaa70b20ac..c550c799ca 100644 --- a/pms_api_rest/services/pms_product_service.py +++ b/pms_api_rest/services/pms_product_service.py @@ -53,7 +53,6 @@ def get_products(self, product_search_param): perDay=product.per_day, perPerson=product.per_person, consumedOn=product.consumed_on, - taxesPercentage=product.taxes_id.amount if product.taxes_id.amount_type == 'percent' else None, ) ) return result_products diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index d1cad6148f..5c2959c81a 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -206,7 +206,8 @@ def update_reservation(self, reservation_id, reservation_data): reservation_vals = self._create_vals_from_params( reservation_vals, reservation_data ) - if reservation_data.boardServiceId == 0: + # TODO: this should be @ pms core + if reservation_data.boardServiceId != reservation.board_service_room_id: reservation.service_ids.filtered(lambda x: x.is_board_service).unlink() if reservation_vals: reservation.write(reservation_vals) From 0880d17ec89a0eb192bcc14def64e525b2335e25 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 29 Nov 2022 16:59:04 +0100 Subject: [PATCH 315/547] [FIX] pms-api-rest: fix precommit --- pms_api_rest/services/pms_reservation_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 5c2959c81a..282933f995 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -212,7 +212,6 @@ def update_reservation(self, reservation_id, reservation_data): if reservation_vals: reservation.write(reservation_vals) - 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) From fb675d1bb7e5d59701c4161b515f7d5c254d91ac Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 29 Nov 2022 12:23:16 +0100 Subject: [PATCH 316/547] [IMP]pms_api_rest: added overpayment color to configurate in property --- pms/models/pms_property.py | 79 ------------------- pms/views/pms_property_views.xml | 67 ---------------- pms_api_rest/datamodels/pms_property.py | 1 + pms_api_rest/models/pms_property.py | 6 ++ pms_api_rest/services/pms_property_service.py | 1 + pms_api_rest/views/pms_property_views.xml | 5 ++ 6 files changed, 13 insertions(+), 146 deletions(-) diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index cb699f7119..63ff0ee50c 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -243,85 +243,6 @@ class PmsProperty(models.Model): default=False, ) - 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)", - ) - @api.depends_context( "checkin", "checkout", diff --git a/pms/views/pms_property_views.xml b/pms/views/pms_property_views.xml index 8da488f716..415f260326 100644 --- a/pms/views/pms_property_views.xml +++ b/pms/views/pms_property_views.xml @@ -88,73 +88,6 @@ - - - - - - - - - - - - - - - - - - - diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index 0e2a7c6bfc..97e280ef7c 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -23,6 +23,7 @@ class PmsPropertyInfo(Datamodel): 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) diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index 80ea18de5e..cda0879373 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -82,3 +82,9 @@ class PmsProperty(models.Model): 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)", + ) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 6818860fa3..51322e4f08 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -43,6 +43,7 @@ def get_properties(self): 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, diff --git a/pms_api_rest/views/pms_property_views.xml b/pms_api_rest/views/pms_property_views.xml index cc3afd76e2..5c86deee6a 100644 --- a/pms_api_rest/views/pms_property_views.xml +++ b/pms_api_rest/views/pms_property_views.xml @@ -70,6 +70,11 @@ widget="color" attrs="{'invisible': [('color_option_config', '!=', 'advanced')]}" /> + From ec06db1e43d3c49187c89bdf64d9e209e9abe65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 1 Dec 2022 14:55:39 +0100 Subject: [PATCH 317/547] [FIX]pms_api_rest: dont link services with boardServiceId param None --- pms_api_rest/services/pms_reservation_service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 282933f995..fbce7bdcd1 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -207,7 +207,10 @@ def update_reservation(self, reservation_id, reservation_data): reservation_vals, reservation_data ) # TODO: this should be @ pms core - if reservation_data.boardServiceId != reservation.board_service_room_id: + if ( + reservation_data.boardServiceId is not None + and reservation_data.boardServiceId != reservation.board_service_room_id + ): reservation.service_ids.filtered(lambda x: x.is_board_service).unlink() if reservation_vals: reservation.write(reservation_vals) From c5de3674c83144795ed22a73734e7bbc24b6f8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 7 Dec 2022 11:31:27 +0100 Subject: [PATCH 318/547] [IMP]pms_api_rest: unique board_service_room_type by property --- .../services/pms_board_service_service.py | 16 ++-------------- pms_api_rest/services/pms_price_service.py | 3 ++- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index 4281077c50..40c2b9dde8 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -36,13 +36,7 @@ def get_board_services(self, board_services_search_param): if board_services_search_param.pmsPropertyId: domain.extend( [ - "|", - ( - "pms_property_ids", - "in", - board_services_search_param.pmsPropertyId, - ), - ("pms_property_ids", "=", False), + ("pms_property_id", "=", board_services_search_param.pmsPropertyId), ] ) @@ -107,13 +101,7 @@ def get_board_service_lines(self, board_service_id, pms_search_param): if pms_search_param.pmsPropertyId: domain.extend( [ - "|", - ( - "pms_property_ids", - "in", - pms_search_param.pmsPropertyId, - ), - ("pms_property_ids", "=", False), + ("pms_property_id", "=", pms_search_param.pmsPropertyId), ] ) result_board_service_lines = [] diff --git a/pms_api_rest/services/pms_price_service.py b/pms_api_rest/services/pms_price_service.py index a7a121896e..164547b430 100644 --- a/pms_api_rest/services/pms_price_service.py +++ b/pms_api_rest/services/pms_price_service.py @@ -151,7 +151,8 @@ def _get_board_service_price( pms_property_id=pms_property_id, pricelist_id=pricelist_id, partner_id=partner_id, - product_qty=product_qty, + product_qty=product_qty or 1, date_consumption=date_consumption, + board_service_id=board_service.id, ) return price From 5c9265461ae99e6c3555424bef3cb8c788759705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 7 Dec 2022 11:56:50 +0100 Subject: [PATCH 319/547] [TMP]: add amenity code in room short name --- pms_api_rest/services/pms_room_service.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 4bd253d0af..702c753666 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -77,14 +77,20 @@ def get_rooms(self, room_search_param): ) .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.name, + name=room.display_name, roomTypeId=room.room_type_id, capacity=room.capacity, - shortName=room.short_name, + shortName=short_name, roomTypeClassId=room.room_type_id.class_id, ubicationId=room.ubication_id, extraBedsAllowed=room.extra_beds_allowed, From 3cfe38fedc8d4228a5e4b67b3bcc72d0a7e186e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 8 Dec 2022 12:45:32 +0100 Subject: [PATCH 320/547] [IMP]pms_api_rest: avoid fail in move lines with multiple folio lines --- pms_api_rest/services/pms_folio_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ca9ae6e8b1..ef138b6c5b 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -662,7 +662,7 @@ def get_folio_invoices(self, folio_id): displayType=move_line.display_type if move_line.display_type else None, - saleLineId=move_line.folio_line_ids + saleLineId=move_line.folio_line_ids[0] if move_line.folio_line_ids else None, isDownPayment=move_line.move_id._is_downpayment(), From 5cbcec5129e2ea13697e303561b511eab3265401 Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 12 Dec 2022 11:10:56 +0100 Subject: [PATCH 321/547] [IMP]: pms-pwa: added folio services GET service --- pms_api_rest/services/pms_folio_service.py | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ef138b6c5b..3c83762142 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -560,6 +560,63 @@ def update_folio(self, folio_id, pms_folio_info): 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( [ ( From 4344db350658e171a0fd51caaefe05fe95d9a894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 14 Dec 2022 11:44:34 +0100 Subject: [PATCH 322/547] [RFC]pms_api_rest: disables subquery services daily invoices in calendar get --- pms_api_rest/services/pms_calendar_service.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 16bc4aa243..cc6c7b8dae 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -30,14 +30,14 @@ def get_calendar(self, calendar_search_param): 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 - subselect_sum_services_price = ( - "(" - " SELECT COALESCE(SUM(s.price_day_total),0) price_day_total_services " - " FROM pms_service_line s " - " WHERE s.reservation_id = night.reservation_id " - " AND s.date = night.date AND NOT s.is_board_service " - " ) " - ) + # subselect_sum_services_price = ( + # "(" + # " SELECT COALESCE(SUM(s.price_day_total),0) price_day_total_services " + # " FROM pms_service_line s " + # " WHERE s.reservation_id = night.reservation_id " + # " AND s.date = night.date AND NOT s.is_board_service " + # " ) " + # ) selected_fields_mapper = { "id": "night.id", "state": "night.state", @@ -58,7 +58,7 @@ def get_calendar(self, calendar_search_param): "folio_pending_amount": "folio.pending_amount", "adults": "reservation.adults", "price_day_total": "night.price_day_total", - "price_day_total_services": subselect_sum_services_price, + # "price_day_total_services": subselect_sum_services_price, } selected_fields_sql = list(selected_fields_mapper.values()) selected_fields = list(selected_fields_mapper.keys()) @@ -142,7 +142,8 @@ def get_calendar(self, calendar_search_param): totalPrice=round(line["price_total"], 2), pendingPayment=round(line["folio_pending_amount"], 2), priceDayTotal=round(line["price_day_total"], 0), - priceDayTotalServices=round(line["price_day_total_services"], 0), + # TODO: priceDayTotalServices=round(line["price_day_total_services"], 0), + priceDayTotalServices=0, # TODO: line.reservation_id.message_needaction_counter is computed field, numNotifications=0, adults=line["adults"], From c30a8c04b1d8e87dbefa31fe95c4e17342703f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 14 Dec 2022 11:46:16 +0100 Subject: [PATCH 323/547] [RFC]pms_api_rest: disabled user image GET by performance issues --- pms_api_rest/services/pms_property_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 51322e4f08..0684b17b68 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -112,7 +112,9 @@ def get_users(self, pms_property_id): ResUsersInfo( id=user.id, name=user.name, - userImageBase64=user.partner_id.image_1024 or None, + # TODO: Disabled by performance issues + # userImageBase64=user.partner_id.image_1024 or None, + userImageBase64=None, ) ) return result_users From e6a9ad239c03b146680fdd6b77d158893bae16f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 14 Dec 2022 11:46:48 +0100 Subject: [PATCH 324/547] [FIX]pms_api_rest: patch invoice without lines in vals --- pms_api_rest/services/pms_invoice_service.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 35167fa15a..a2a4338cf7 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -109,17 +109,19 @@ def _direct_move_update(self, invoice, new_vals): 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. + # so, we need to save and rewrite it. (core odoo methods) # 1- save send invoice line name values: - 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] - } + 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] + } # 2- update invoice invoice.write(new_vals) # 3- rewrite invoice line name values: From e5e976269b583323241f78066976c9eb36c413c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 14 Dec 2022 19:28:53 +0100 Subject: [PATCH 325/547] [FIX]pms_api_rest: PmsPriceService name Class --- pms_api_rest/services/pms_price_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_price_service.py b/pms_api_rest/services/pms_price_service.py index 164547b430..09f5d8af3e 100644 --- a/pms_api_rest/services/pms_price_service.py +++ b/pms_api_rest/services/pms_price_service.py @@ -8,7 +8,7 @@ from odoo.addons.component.core import Component -class PmsAgencyService(Component): +class PmsPriceService(Component): _inherit = "base.rest.service" _name = "pms.price.service" _usage = "prices" From cacbe8251798dc79b9e4ad040f8ce46cff3965ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 21 Dec 2022 14:48:27 +0100 Subject: [PATCH 326/547] [FIX]pms_api_rest: alerts per day get overbooking lines --- pms_api_rest/services/pms_calendar_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index cc6c7b8dae..47245b405f 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -409,7 +409,9 @@ def get_alerts_per_day(self, pms_calendar_search_param): PmsCalendarAlertsPerDay = self.env.datamodels["pms.calendar.alerts.per.day"] result = [] for day in target_dates: - overbooking_lines = next((item for item in result_sql if item[0] == day), 0) + 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()), From 564f2fd6aad14eb9698f596894db3a2b3eb7309e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 24 Dec 2022 16:16:03 +0100 Subject: [PATCH 327/547] [IMP]pms_api_rest: localize date done cash register --- pms_api_rest/services/pms_transaction_service.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 2d93af0527..f88db5ab8b 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -446,6 +446,10 @@ def get_cash_register(self, cash_register_search_param): 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", @@ -453,8 +457,8 @@ def get_cash_register(self, cash_register_search_param): balance=statement.balance_start if isOpen else statement.balance_end_real, dateTime=create_date.isoformat() if isOpen - else statement.date_done.isoformat() - if statement.date_done + else date_done.isoformat() + if date_done else None, ) From 3072c3c4791775737a5bcdc18568b9fef22d8eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 6 Apr 2023 10:31:49 +0200 Subject: [PATCH 328/547] [IMP] pms-api-rest: several changes to takt into account out-of-service reservations --- pms_api_rest/datamodels/pms_calendar.py | 3 +-- pms_api_rest/datamodels/pms_folio.py | 3 +++ pms_api_rest/datamodels/pms_reservation.py | 2 +- pms_api_rest/services/pms_calendar_service.py | 6 ++---- pms_api_rest/services/pms_folio_service.py | 13 +++++++++++-- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 9e54796ae1..7b7483fadb 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -64,10 +64,9 @@ class PmsCalendarInfo(Datamodel): pendingPayment = fields.Float(required=False, allow_none=True) numNotifications = fields.Integer(required=False, allow_none=True) adults = fields.Integer(required=False, allow_none=True) - hasNextLine = fields.Boolean(required=False, allow_none=True) nextLineSplitted = fields.Boolean(required=False, allow_none=True) previousLineSplitted = fields.Boolean(required=False, allow_none=True) - closureReason = fields.String(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) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 75375556b5..9efb69ea37 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -34,6 +34,7 @@ class PmsFolioInfo(Datamodel): 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 @@ -53,3 +54,5 @@ class PmsFolioShortInfo(Datamodel): 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)) + reservationType = fields.String(required=False, allow_none=True) + closureReasonId = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 0c69fbb489..b139b37aaa 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -19,7 +19,7 @@ class PmsReservationShortInfo(Datamodel): paymentState = fields.String(required=False, allow_none=True) readyForCheckin = fields.Boolean(required=False, allow_none=True) allowedCheckout = fields.Boolean(required=False, allow_none=True) - splitted = 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) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 47245b405f..2e9b337633 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -58,6 +58,7 @@ def get_calendar(self, calendar_search_param): "folio_pending_amount": "folio.pending_amount", "adults": "reservation.adults", "price_day_total": "night.price_day_total", + "closure_reason_id": "folio.closure_reason_id", # "price_day_total_services": subselect_sum_services_price, } selected_fields_sql = list(selected_fields_mapper.values()) @@ -149,10 +150,7 @@ def get_calendar(self, calendar_search_param): adults=line["adults"], nextLineSplitted=next_line_splitted, previousLineSplitted=previous_line_splitted, - hasNextLine=not is_last_night, # REVIEW: redundant with isLastNight? - closureReason=line[ - "partner_name" - ], # REVIEW: is necesary closure_reason_id? + closureReasonId=line["closure_reason_id"], ) ) return result_lines diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 3c83762142..db5cc2b78e 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -61,6 +61,10 @@ def get_folio(self, folio_id): 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, ) else: raise MissingError(_("Folio not found")) @@ -156,7 +160,7 @@ def get_folios(self, folio_search_param): "agencyId": reservation.agency_id.id if reservation.agency_id else None, - "splitted": reservation.splitted, + "isSplitted": reservation.splitted, } ) result_folios.append( @@ -173,6 +177,8 @@ def get_folios(self, folio_search_param): "selection" ] )[folio.payment_state], + reservationType=folio.reservation_type, + closureReasonId=folio.closure_reason_id, ) ) return result_folios @@ -370,7 +376,7 @@ def get_folio_reservations(self, folio_id): else None, readyForCheckin=reservation.ready_for_checkin, allowedCheckout=reservation.allowed_checkout, - splitted=reservation.splitted, + isSplitted=reservation.splitted, priceTotal=round(reservation.price_room_services_set, 2), servicesCount=sum( reservation.service_ids.filtered( @@ -400,6 +406,9 @@ def create_folio(self, pms_folio_info): "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 = { From 8ee8b79c94f33ddf75121a8ece609bdf619a7317 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 23 Dec 2022 14:13:53 +0100 Subject: [PATCH 329/547] [IMP]pms_api_rest: added filter by state and checkin/checkout in folios get service --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 34 ++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 9efb69ea37..04d66796f1 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -10,6 +10,7 @@ class PmsFolioSearchParam(Datamodel): 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) class PmsFolioInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index db5cc2b78e..e426fdb389 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -109,7 +109,7 @@ def get_folios(self, folio_search_param): if folio_search_param.filter: target = folio_search_param.filter if "@" in target: - domain_filter.append(("email", "ilike", target)) + domain_filter.append([("email", "ilike", target)]) else: subdomains = [ [("name", "ilike", target)], @@ -118,9 +118,39 @@ def get_folios(self, folio_search_param): [("external_reference", "ilike", target)], ] domain_filter.append(expression.OR(subdomains)) - domain = [] + if folio_search_param.filterByState: + if folio_search_param.filterByState == "byCheckin": + subdomains = [ + [("state", "in", ("confirm", "arrival_delayed"))], + [("checkin", "<=", fields.Date.today())], + ] + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == "byCheckout": + subdomains = [ + [("state", "in", ("onboard", "departure_delayed"))], + [("checkout", "=", fields.Date.today())], + ] + domain_filter.append(expression.AND(subdomains)) + else: + subdomain_checkin = [ + [("state", "in", ("confirm", "arrival_delayed"))], + [("checkin", "<=", fields.Date.today())], + ] + subdomain_checkin = expression.AND(subdomain_checkin) + subdomain_checkout = [ + [("state", "in", ("onboard", "departure_delayed"))], + [("checkout", "=", fields.Date.today())], + ] + subdomain_checkout = expression.AND(subdomain_checkout) + domain_filter.append( + expression.OR([subdomain_checkin, subdomain_checkout]) + ) 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 = [] From 7ba3eecbe59bfa74d448f109bca8a933169d6f1d Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 28 Dec 2022 19:41:52 +0100 Subject: [PATCH 330/547] [IMP]pms_api_rest: partner name, email and mobile in folio PATCH --- pms_api_rest/services/pms_folio_service.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index e426fdb389..40b4ddc0b8 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -39,6 +39,7 @@ def get_folio(self, folio_id): 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, @@ -558,6 +559,17 @@ def update_folio(self, folio_id, pms_folio_info): reservation.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.reservations: for reservation in pms_folio_info.reservations: vals = { From 335a636609a014716367723d2f7b54d36d35bfa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 29 Dec 2022 12:41:47 +0100 Subject: [PATCH 331/547] [IMP]pms_api_rest: imrpovement performance folio get service --- pms_api_rest/services/pms_folio_service.py | 33 ++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 40b4ddc0b8..5f0945b709 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -85,8 +85,8 @@ def get_folio(self, folio_id): ) def get_folios(self, folio_search_param): domain_fields = list() - - domain_fields.append(("pms_property_id", "=", folio_search_param.pmsPropertyId)) + pms_property_id = int(folio_search_param.pmsPropertyId) + domain_fields.append(("pms_property_id", "=", pms_property_id)) if folio_search_param.dateTo and folio_search_param.dateFrom: date_from = fields.Date.from_string(folio_search_param.dateFrom) @@ -95,16 +95,25 @@ def get_folios(self, folio_search_param): date_from + timedelta(days=x) for x in range(0, (date_to - date_from).days + 1) ] - reservation_lines = list( - set( - self.env["pms.reservation.line"] - .search([("date", "in", dates)]) - .mapped("reservation_id") - .mapped("folio_id") - .ids - ) + 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), + ), ) - domain_fields.append(("folio_id", "in", reservation_lines)) + 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.filter: @@ -124,12 +133,14 @@ def get_folios(self, folio_search_param): 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)) else: From 70a9e1ce1de39398fb19610fed71a71bf904bf25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 30 Dec 2022 12:28:30 +0100 Subject: [PATCH 332/547] [IMP]pms_api_rest: improvement compute invoice sequence lines --- pms_api_rest/services/pms_folio_service.py | 7 ++- pms_api_rest/services/pms_invoice_service.py | 63 ++++++++++++++++---- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 5f0945b709..1882c791a6 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -842,8 +842,9 @@ 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 @@ -851,6 +852,10 @@ def create_folio_invoices(self, folio_id, invoice_info): 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.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, diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index a2a4338cf7..437a3291b0 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -56,7 +56,7 @@ def update_invoice(self, invoice_id, pms_invoice_info): if new_vals: # Update Invoice # When modifying an invoice, depending on the company's configuration, - # and the invoice stateit will be modified directly or a reverse + # 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 @@ -98,11 +98,31 @@ def update_invoice(self, invoice_id, pms_invoice_info): 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() - return invoice.id + 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 def _direct_move_update(self, invoice, new_vals): previus_state = invoice.state @@ -122,6 +142,16 @@ def _direct_move_update(self, invoice, new_vals): 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 + 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: @@ -209,7 +239,7 @@ def _get_invoice_lines_commands(self, invoice, pms_invoice_info): line_values["quantity"] = line_info.quantity if line_values: cmd_invoice_lines.append((1, line.id, line_values)) - else: + elif not line.display_type: cmd_invoice_lines.append((2, line.id)) # Get the new lines to add in invoice newInvoiceLinesInfo = list( @@ -234,20 +264,33 @@ def _get_invoice_lines_commands(self, invoice, pms_invoice_info): } ) ) + 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={ - newInvoiceLinesInfo[i] - .saleLineId: newInvoiceLinesInfo[i] - .quantity - for i in range(0, len(newInvoiceLinesInfo)) - }, + lines_to_invoice=lines_to_invoice, partner_invoice_id=partner.id, ) ][0] # Update name of new invoice lines - for item in 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 From 5696394a4ecbbc0026e1e0290580ff179da6b0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 30 Dec 2022 17:42:56 +0100 Subject: [PATCH 333/547] [IMP]pms_api_rest: draft invoice use invoice_date_due --- pms_api_rest/services/pms_folio_service.py | 11 ++++++++--- pms_api_rest/services/pms_invoice_service.py | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 1882c791a6..ad066c6145 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -796,6 +796,13 @@ def get_folio_invoices(self, folio_id): 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, @@ -803,9 +810,7 @@ def get_folio_invoices(self, folio_id): amount=round(move.amount_total, 2) if move.amount_total else None, - date=move.invoice_date.strftime("%d/%m/%Y") - if move.invoice_date - else None, + date=invoice_date, state=move.state if move.state else None, paymentState=move.payment_state if move.payment_state diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 437a3291b0..d44658eda1 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -146,6 +146,8 @@ def _direct_move_update(self, invoice, new_vals): # 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 From 13a378555fb6c603bcd4f270648a29721ddfec5b Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 4 Jan 2023 21:37:40 +0100 Subject: [PATCH 334/547] [IMP]pms_api_rest: added toAssign, name and partnerName in folios service GET --- pms_api_rest/services/pms_folio_service.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ad066c6145..aae4aca862 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -180,6 +180,11 @@ def get_folios(self, folio_search_param): reservations.append( { "id": reservation.id, + "folioId": folio.id, + "name": reservation.name, + "partnerName": reservation.partner_name + if reservation.partner_name + else None, "checkin": datetime.combine( reservation.checkin, datetime.min.time() ).isoformat(), @@ -203,6 +208,7 @@ def get_folios(self, folio_search_param): if reservation.agency_id else None, "isSplitted": reservation.splitted, + "toAssign": reservation.to_assign, } ) result_folios.append( From 323ea48cf72158fe50f1e190f0b66c7b9b68ab33 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 5 Jan 2023 17:40:01 +0100 Subject: [PATCH 335/547] [IMP]pms-api_rest: GET reservations services with toAssign param --- pms_api_rest/datamodels/pms_search_param.py | 1 + pms_api_rest/services/pms_folio_service.py | 6 -- .../services/pms_reservation_service.py | 92 +++++++++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/datamodels/pms_search_param.py b/pms_api_rest/datamodels/pms_search_param.py index d7951a104f..dc7f79001a 100644 --- a/pms_api_rest/datamodels/pms_search_param.py +++ b/pms_api_rest/datamodels/pms_search_param.py @@ -8,3 +8,4 @@ class PmsSearchParam(Datamodel): pmsPropertyId = fields.Integer(required=False, allow_none=True) pmsPropertyIds = fields.List(fields.Integer(), required=False) + toAssign = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index aae4aca862..ad066c6145 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -180,11 +180,6 @@ def get_folios(self, folio_search_param): reservations.append( { "id": reservation.id, - "folioId": folio.id, - "name": reservation.name, - "partnerName": reservation.partner_name - if reservation.partner_name - else None, "checkin": datetime.combine( reservation.checkin, datetime.min.time() ).isoformat(), @@ -208,7 +203,6 @@ def get_folios(self, folio_search_param): if reservation.agency_id else None, "isSplitted": reservation.splitted, - "toAssign": reservation.to_assign, } ) result_folios.append( diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index fbce7bdcd1..0023ccc381 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -573,6 +573,98 @@ def write_reservation_checkin_partner( ) return checkin_partner.id + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + input_param=Datamodel("pms.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())) + 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, + 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( [ ( From 7d9c56087917ddebe004736f0892fe1507fcedb4 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 3 Jan 2023 22:27:48 +0100 Subject: [PATCH 336/547] [FIX]pms_api_rest: get, post and patch checkin partner nationality_id --- pms_api_rest/services/pms_reservation_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 0023ccc381..fb0b12529d 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -529,8 +529,8 @@ def get_checkin_partners(self, reservation_id): residenceCity=checkin_partner.residence_city if checkin_partner.residence_city else None, - nationality=checkin_partner.residence_country_id.id - if checkin_partner.residence_country_id + 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 @@ -796,7 +796,7 @@ def mapping_checkin_partner_values(self, pms_checkin_partner_info): "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.countryId, + "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, From f027bfc84c937aa4eee7a2a6c36bfe801cf508d9 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 24 Nov 2022 13:35:11 +0100 Subject: [PATCH 337/547] [IMP]pms-pwa: added service header for folio mails --- pms_api_rest/datamodels/pms_mail.py | 1 + pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/services/pms_folio_service.py | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/pms_api_rest/datamodels/pms_mail.py b/pms_api_rest/datamodels/pms_mail.py index 211335834f..ba66a50ebc 100644 --- a/pms_api_rest/datamodels/pms_mail.py +++ b/pms_api_rest/datamodels/pms_mail.py @@ -5,6 +5,7 @@ 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) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index b139b37aaa..fbb14955c4 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -22,6 +22,7 @@ class PmsReservationShortInfo(Datamodel): 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) class PmsReservationInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ad066c6145..692cda068f 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -420,6 +420,9 @@ def get_folio_reservations(self, folio_id): 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, servicesCount=sum( reservation.service_ids.filtered( lambda x: not x.is_board_service @@ -679,6 +682,21 @@ def get_folio_services(self, folio_id): ) return result_services + @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): + return True + @restapi.method( [ ( From 5d0ab5dbaceb4235dfb89a6dd2be0d40ae56b719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 24 Dec 2022 17:37:47 +0100 Subject: [PATCH 338/547] [ADD]pms_api_rest: workflow folio mails --- pms_api_rest/services/pms_folio_service.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 692cda068f..032a67c804 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -695,6 +695,32 @@ def get_folio_services(self, folio_id): 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( From c54838056eefe6e32dcc48d712b64f1599cd8e28 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 13 Jan 2023 19:35:30 +0100 Subject: [PATCH 339/547] [WIP]pms_api_rest: service get folio and invoice mail --- pms_api_rest/services/pms_folio_service.py | 20 ++++++++++++++++++++ pms_api_rest/services/pms_invoice_service.py | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 032a67c804..d5f4d1b99b 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -682,6 +682,26 @@ def get_folio_services(self, folio_id): ) 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): + PmsMailInfo = self.env.datamodels["pms.mail.info"] + + return PmsMailInfo( + bodyMail="Oaosiaosi ", + subject="Aasdadsasd" + ) @restapi.method( [ ( diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index d44658eda1..964b4cd1bf 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -190,6 +190,25 @@ def create_invoice(self, pms_invoice_info): 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): + PmsMailInfo = self.env.datamodels["pms.mail.info"] + + return PmsMailInfo( + bodyMail="Jaskdjh kaksjdh", + subject="Aasdadsasd", + ) @restapi.method( [ ( From 57a3f935da710d3ea1303883a348d11e6cc2ca1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 17 Jan 2023 17:43:33 +0100 Subject: [PATCH 340/547] [IMP]pms_api_rest: add compose to mail service --- pms_api_rest/services/pms_folio_service.py | 36 +++++++++++++++++--- pms_api_rest/services/pms_invoice_service.py | 17 +++++++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d5f4d1b99b..3eef314c4d 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -695,13 +695,41 @@ def get_folio_services(self, folio_id): output_param=Datamodel("pms.mail.info", is_list=False), auth="jwt_api_pms", ) - def get_folio_mail(self, folio_id): + 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="Oaosiaosi ", - subject="Aasdadsasd" + bodyMail=values["body"], + subject=values["subject"], ) + @restapi.method( [ ( diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 964b4cd1bf..4b83427ee9 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -203,12 +203,23 @@ def create_invoice(self, pms_invoice_info): 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="Jaskdjh kaksjdh", - subject="Aasdadsasd", + bodyMail=values["body"], + subject=values["subject"], ) + @restapi.method( [ ( From 7cde3937b1342d545973927b46f28b4518506eea Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 17 Jan 2023 11:33:05 +0100 Subject: [PATCH 341/547] [IMP]pms_api_rest: added portal_url field in folio GET service --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 04d66796f1..d920a43f3c 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -43,6 +43,7 @@ class PmsFolioInfo(Datamodel): 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) class PmsFolioShortInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 3eef314c4d..127ddc68a0 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -35,6 +35,10 @@ def get_folio(self, 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, @@ -66,6 +70,7 @@ def get_folio(self, folio_id): outOfServiceDescription=folio.out_service_description if folio.out_service_description else None, + portalUrl=portal_url, ) else: raise MissingError(_("Folio not found")) From a98999072d9196066219b23c15ec2c7b361285df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 19 Jan 2023 20:45:58 +0100 Subject: [PATCH 342/547] [IMP]pms_api_rest: add date_to in services report --- pms_api_rest/services/pms_service_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_service_service.py b/pms_api_rest/services/pms_service_service.py index 4c93c1de60..4d367cc915 100644 --- a/pms_api_rest/services/pms_service_service.py +++ b/pms_api_rest/services/pms_service_service.py @@ -188,12 +188,13 @@ def get_service_lines(self, service_id): 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" From 2cb3c4a636ee558dd5793e1ad40cfd864226f47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 19 Jan 2023 20:51:51 +0100 Subject: [PATCH 343/547] [IMP]pms_api_rest: partnet search mail explicit with @ --- pms_api_rest/services/pms_partner_service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 453db7d540..e72f813ecd 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -61,8 +61,11 @@ def get_partners(self, pms_partner_search_params): ) ], [("display_name", "ilike", pms_partner_search_params.filter)], - [("email", "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"] From a596ca4e0daeed725d1d7a108457c94675414850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 19 Jan 2023 20:53:28 +0100 Subject: [PATCH 344/547] [IMP]pms_api_rest: add date_to in services report --- pms_api_rest/data/sql_reports.xml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/data/sql_reports.xml b/pms_api_rest/data/sql_reports.xml index 3e9626ec9d..d60dacb052 100644 --- a/pms_api_rest/data/sql_reports.xml +++ b/pms_api_rest/data/sql_reports.xml @@ -9,6 +9,15 @@ manual + + x_date_to + Date + date + + sql.file.wizard + manual + + x_pms_property_id Property @@ -77,7 +86,7 @@ ORDER BY reservation.name @@ -111,7 +120,8 @@ 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) + WHERE (line.date >= %(x_date_from)s) + AND (line.date <= %(x_date_to)s) AND (line.pms_property_id = %(x_pms_property_id)s) GROUP BY line.id, product_tmpl.name, reservation.name, reservation.rooms, reservation.adults, reservation.children; From 2af611b9379ea652e7cf6ef70f568cc6af57bce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 19 Jan 2023 20:54:32 +0100 Subject: [PATCH 345/547] [FIX]pms_api_rest: avoid False in invoice line sections --- pms_api_rest/services/pms_folio_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 127ddc68a0..d5c5c6a35c 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -955,7 +955,7 @@ def create_folio_invoices(self, folio_id, invoice_info): lines_to_invoice_dict.keys() ) for line in sale_lines_to_invoice: - if line.section_id.id not in sale_lines_to_invoice.ids: + 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 From c45d22c71a46a34a3a861622a39b890ce6bc0a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 20 Jan 2023 09:28:19 +0100 Subject: [PATCH 346/547] [TMP]pms_api_rest: temporally avoid xml error --- pms_api_rest/data/sql_reports.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pms_api_rest/data/sql_reports.xml b/pms_api_rest/data/sql_reports.xml index d60dacb052..391095cc8d 100644 --- a/pms_api_rest/data/sql_reports.xml +++ b/pms_api_rest/data/sql_reports.xml @@ -121,7 +121,6 @@ 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) GROUP BY line.id, product_tmpl.name, reservation.name, reservation.rooms, reservation.adults, reservation.children; From 416c3aa7fb03c9962c36759d8e972ea862a920eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 22 Jan 2023 12:20:21 +0100 Subject: [PATCH 347/547] [IMP]pms_api_rest: force update name in line invoices to avoid overwrite --- pms_api_rest/services/pms_invoice_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 4b83427ee9..f060e4bb9c 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -265,7 +265,7 @@ def _get_invoice_lines_commands(self, invoice, pms_invoice_info): if line_info: line_info = line_info[0] line_values = {} - if line_info.name and line_info.name != line.name: + 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 From 4aa44de3edf84cff02a591631d0345b175269438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 29 Jan 2023 11:10:47 +0100 Subject: [PATCH 348/547] [IMP]connector_pms: checkin partner search document number improvements --- pms_api_rest/services/pms_partner_service.py | 12 +++++++++++- pms_api_rest/services/pms_reservation_service.py | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index e72f813ecd..d2a320f029 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -1,3 +1,4 @@ +import re from datetime import datetime from odoo.osv import expression @@ -306,8 +307,17 @@ def get_partner_by_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 + ) + if partner.id_numbers: + doc_number = partner.id_numbers[0] + doc_number = self.env["res.partner.id_number"].search( - [("name", "=", document_number), ("category_id", "=", doc_type.id)] + [("name", "ilike", document_number), ("category_id", "=", doc_type.id)], + limit=1, ) partners = [] PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index fb0b12529d..799023aa56 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -594,6 +594,7 @@ def get_reservations(self, pms_search_param): if pms_search_param.toAssign: domain.append(("to_assign", "=", True)) domain.append(("checkin", ">=", fields.Date.today())) + domain.append(("state", "!=", "cancel")) reservations = self.env["pms.reservation"].search(domain) PmsReservationInfo = self.env.datamodels["pms.reservation.info"] if not reservations: From 8c6ac1b055cfe334ee6a4102fe8532aa8467733c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 30 Jan 2023 12:25:14 +0100 Subject: [PATCH 349/547] [IMP]pms_api_rest: PATCH reservation check room type id value --- pms_api_rest/services/pms_reservation_service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 799023aa56..5d558bb8d0 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -200,7 +200,10 @@ def update_reservation(self, reservation_id, reservation_data): reservation.confirm() if reservation_data.toCheckout is not None and reservation_data.toCheckout: reservation.action_reservation_checkout() - if reservation_data.roomTypeId: + if ( + reservation_data.roomTypeId + and reservation.room_type_id.id != reservation_data.roomTypeId + ): reservation.room_type_id = reservation_data.roomTypeId reservation_vals = self._create_vals_from_params( From f6c92de04d94926e255c8f208085a26cac0a73bd Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 1 Feb 2023 20:01:55 +0100 Subject: [PATCH 350/547] [IMP]pms-api_rest: added partner_requests field in fetch and patch reservation services --- pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/services/pms_reservation_service.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index fbb14955c4..ef25b08736 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -72,6 +72,7 @@ class PmsReservationInfo(Datamodel): services = fields.List( NestedModel("pms.service.info"), required=False, allow_none=True ) + partnerRequests = fields.String(required=False, allow_none=True) # TODO: Refact # messages = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 5d558bb8d0..9c97826074 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -114,6 +114,9 @@ def get_reservation(self, reservation_id, pms_search_param): else None, priceOnlyServices=round(reservation.price_services, 2), priceOnlyRoom=round(reservation.price_total, 2), + partnerRequests=reservation.partner_requests + if reservation.partner_requests + else None, ) return res @@ -136,6 +139,10 @@ def _create_vals_from_params(self, reservation_vals, reservation_data): reservation_vals.update( {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} ) + if reservation_data.partnerRequests is not None: + reservation_vals.update( + {"partner_requests": reservation_data.partnerRequests} + ) return reservation_vals @restapi.method( From d1da47961a3c95cb04127fdb5d0b6ad0376da25f Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 3 Feb 2023 19:00:52 +0100 Subject: [PATCH 351/547] [IMP]14.0-pms_api_rest: added return transaction ids for folio after create new charge --- pms_api_rest/services/pms_folio_service.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d5c5c6a35c..d72f8bb091 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -330,6 +330,13 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): partner=partner_id, date=datetime.strptime(pms_account_payment_info.date, "%m/%d/%Y"), ) + folio_transactions = self.env["account.payment"].search( + [ + ("folio_ids", "in", folio_id), + ("pms_api_transaction_type", "=", "customer_inbound"), + ] + ) + return folio_transactions.ids @restapi.method( [ From 7ecbe14555ff96d172a5602d49578803e5ef9c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 4 Feb 2023 10:51:58 +0100 Subject: [PATCH 352/547] [RFC]pms_apie_rest: return folio transactions Avoid search in all transactions --- pms_api_rest/services/pms_folio_service.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d72f8bb091..4f84023f8a 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -330,11 +330,8 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): partner=partner_id, date=datetime.strptime(pms_account_payment_info.date, "%m/%d/%Y"), ) - folio_transactions = self.env["account.payment"].search( - [ - ("folio_ids", "in", folio_id), - ("pms_api_transaction_type", "=", "customer_inbound"), - ] + folio_transactions = folio.payment_ids.filtered( + lambda p: p.folio_transactions == "customer_inbound" ) return folio_transactions.ids From 86311cd8434577cb879ce12e70458f31d19bfc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 4 Feb 2023 11:07:26 +0100 Subject: [PATCH 353/547] [IMP]pms_api_rest: ignore overbooking reservations in calendar --- pms_api_rest/services/pms_calendar_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 2e9b337633..9d137f7ffd 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -77,6 +77,7 @@ def get_calendar(self, calendar_search_param): WHERE (night.pms_property_id = %s) AND (night.date in %s) AND (night.state != 'cancel') + AND (night.occupies_availability = True) """, ( pms_property_id, From 607997cad396b02036c00b0a50bcf14ea6647b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 5 Feb 2023 17:40:32 +0100 Subject: [PATCH 354/547] [IMP]pms_api_rest: compare float field values changes with round --- pms_api_rest/services/pms_reservation_service.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 9c97826074..bf5de43528 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -230,16 +230,19 @@ def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): # or all pass values if line not exist) line_vals = {} if origin_data.price and ( - not reservation_line or origin_data.price != reservation_line.price + 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 origin_data.discount != reservation_line.discount + 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 origin_data.cancelDiscount != reservation_line.cancelDiscount + or round(origin_data.cancelDiscount, 2) + != round(reservation_line.cancelDiscount, 2) ): line_vals["cancel_discount"] = origin_data.cancelDiscount if origin_data.roomId and ( From cc9c7785d478ccc49f71cbdb919fa181706e8ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 5 Feb 2023 11:58:18 +0100 Subject: [PATCH 355/547] [IMP]pms_api_rest: improvement search by document partners --- pms_api_rest/services/pms_partner_service.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index d2a320f029..662eddb7a7 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -308,6 +308,7 @@ def get_partner_by_doc_number(self, document_type, document_number): [("id", "=", document_type)] ) # Clean Document number + doc_number = False 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 @@ -315,10 +316,6 @@ def get_partner_by_doc_number(self, document_type, document_number): if partner.id_numbers: doc_number = partner.id_numbers[0] - doc_number = self.env["res.partner.id_number"].search( - [("name", "ilike", document_number), ("category_id", "=", doc_type.id)], - limit=1, - ) partners = [] PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] if not doc_number: @@ -354,10 +351,10 @@ def get_partner_by_doc_number(self, document_type, document_number): documentType=doc_type.id, documentNumber=doc_number.name, documentExpeditionDate=document_expedition_date - if doc_number.valid_from + if doc_number.valid_from and doc_number.category_id == doc_type else None, documentSupportNumber=doc_number.support_number - if doc_number.support_number + if doc_number.support_number and doc_number.category_id == doc_type else None, gender=doc_number.partner_id.gender if doc_number.partner_id.gender From 12428e8757fad94d39459c074948ac40ff17a74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 6 Feb 2023 08:32:52 +0100 Subject: [PATCH 356/547] [FIX]pms_api_rest: fix filter ield pms_api_transaction_type --- pms_api_rest/services/pms_folio_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 4f84023f8a..52cf269d75 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -331,7 +331,7 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): date=datetime.strptime(pms_account_payment_info.date, "%m/%d/%Y"), ) folio_transactions = folio.payment_ids.filtered( - lambda p: p.folio_transactions == "customer_inbound" + lambda p: p.pms_api_transaction_type == "customer_inbound" ) return folio_transactions.ids From 14e56aefad9647d141a154ad595b6f88f47269d1 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 7 Feb 2023 09:36:05 +0100 Subject: [PATCH 357/547] [IMP]pms_api_rest: added GET service for reservation messages --- pms_api_rest/datamodels/__init__.py | 1 + .../datamodels/pms_reservation_message.py | 20 ++++++ pms_api_rest/services/pms_folio_service.py | 61 +++++++++++++++++-- 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_reservation_message.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 93946d74ef..c6619b677b 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -57,3 +57,4 @@ from . import pms_invoice_line from . import pms_mail from . import pms_notification +from . import pms_reservation_message 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..95d3a84d59 --- /dev/null +++ b/pms_api_rest/datamodels/pms_reservation_message.py @@ -0,0 +1,20 @@ +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) + +class PmsFolioMessageInfo(Datamodel): + _name = "pms.folio.message.info" + folioId = fields.Integer(required=False, allow_none=True) + reservationMessages = fields.List( + NestedModel("pms.reservation.message.info"), required=False, allow_none=True + ) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 52cf269d75..da63462206 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -976,7 +976,7 @@ def create_folio_invoices(self, folio_id, invoice_info): 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]) + and not any([r.folio_line_ids.is_downpayment]) # To avoid modifying down payments description ) if invoice_line: @@ -1013,9 +1013,9 @@ def _action_open_cash_session(self, pms_property_id, amount, journal_id, force): self.env["account.bank.statement"].sudo().create( { "name": datetime.today().strftime(get_lang(self.env).date_format) - + " (" - + self.env.user.login - + ")", + + " (" + + self.env.user.login + + ")", "date": datetime.today(), "balance_start": amount, "journal_id": journal_id, @@ -1051,3 +1051,56 @@ def _get_section_qty_to_invoice(self, sale_line): ).mapped("qty_to_invoice") ) return False + + @restapi.method( + [ + ( + [ + "//messages", + ], + "GET", + ) + ], + auth="jwt_api_pms", + output_param=Datamodel("pms.folio.message.info", is_list=False), + ) + def get_reservation_messages(self, folio_id): + reservation_messages = [] + if folio_id: + folio = self.env["pms.folio"].browse(folio_id) + reservations = self.env['pms.reservation'].browse(folio.reservation_ids.ids) + PmsFolioMessageInfo = self.env.datamodels["pms.folio.message.info"] + + for messages in reservations.message_ids: + PmsReservationMessageInfo = self.env.datamodels["pms.reservation.message.info"] + for message in messages: + 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.email_from if message.email_from else message.author_id[1], + message=message_body, + subject=subject, + date=message.date.strftime("%d/%m/%y %H:%M:%S"), + messageType=message.message_type, + )) + + return PmsFolioMessageInfo( + folioId=folio_id, + reservationMessages=reservation_messages, + ) + + def parse_message_body(self, message): + message_body = '' + if message.body: + message_body = message.body + elif message.tracking_value_ids: + for tracking_value in message.tracking_value_ids: + # changed_field = str(tracking_value.changed_field) or "" + # old_value = str(tracking_value.old_value) or "" + # new_value = str(tracking_value.new_value) or "" + message_body += str(tracking_value)+' falta changed_field, old_value e new_value' + return message_body From 659c1affd2a8168b47a43b49879ae3f60e4245bc Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 9 Feb 2023 12:54:44 +0100 Subject: [PATCH 358/547] [IMP]pms_api_rest: get_folio_reservation_messages reformated to get folio and reservation messages --- pms_api_rest/datamodels/pms_reservation.py | 1 + .../datamodels/pms_reservation_message.py | 17 ++- pms_api_rest/services/pms_folio_service.py | 111 ++++++++++++++---- 3 files changed, 104 insertions(+), 25 deletions(-) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index ef25b08736..f3318b5684 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -12,6 +12,7 @@ class PmsReservationShortInfo(Datamodel): checkout = fields.String(required=False, allow_none=True) roomTypeId = 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) diff --git a/pms_api_rest/datamodels/pms_reservation_message.py b/pms_api_rest/datamodels/pms_reservation_message.py index 95d3a84d59..6ff581da26 100644 --- a/pms_api_rest/datamodels/pms_reservation_message.py +++ b/pms_api_rest/datamodels/pms_reservation_message.py @@ -3,6 +3,7 @@ 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) @@ -11,10 +12,24 @@ class PmsReservationMessageInfo(Datamodel): 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) + class PmsFolioMessageInfo(Datamodel): _name = "pms.folio.message.info" - folioId = 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) + + +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/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index da63462206..aca0197aae 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1,3 +1,4 @@ +import base64 from datetime import datetime, timedelta from odoo import _, fields @@ -417,6 +418,7 @@ def get_folio_reservations(self, folio_id): preferredRoomId=reservation.preferred_room_id.id if reservation.preferred_room_id else None, + name=reservation.name, adults=reservation.adults, stateCode=reservation.state, stateDescription=dict( @@ -976,7 +978,7 @@ def create_folio_invoices(self, folio_id, invoice_info): 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]) + and not any([r.folio_line_ids.is_downpayment]) # To avoid modifying down payments description ) if invoice_line: @@ -1013,9 +1015,9 @@ def _action_open_cash_session(self, pms_property_id, amount, journal_id, force): self.env["account.bank.statement"].sudo().create( { "name": datetime.today().strftime(get_lang(self.env).date_format) - + " (" - + self.env.user.login - + ")", + + " (" + + self.env.user.login + + ")", "date": datetime.today(), "balance_start": amount, "journal_id": journal_id, @@ -1062,45 +1064,106 @@ def _get_section_qty_to_invoice(self, sale_line): ) ], auth="jwt_api_pms", - output_param=Datamodel("pms.folio.message.info", is_list=False), + output_param=Datamodel("pms.message.info", is_list=False), ) - def get_reservation_messages(self, folio_id): + 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) - PmsFolioMessageInfo = self.env.datamodels["pms.folio.message.info"] - + reservations = self.env["pms.reservation"].browse(folio.reservation_ids.ids) for messages in reservations.message_ids: - PmsReservationMessageInfo = self.env.datamodels["pms.reservation.message.info"] + PmsReservationMessageInfo = self.env.datamodels[ + "pms.reservation.message.info" + ] for message in messages: 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.email_from if message.email_from else message.author_id[1], + reservation_messages.append( + PmsReservationMessageInfo( + reservationId=message.res_id, + author=message.email_from + if message.email_from + else message.author_id[0], + message=message_body, + subject=subject, + date=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, + ) + ) + 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_messages.append( + PmsFolioMessageInfo( + author=folio_message.email_from + if folio_message.email_from + else folio_message.author_id[1], message=message_body, subject=subject, - date=message.date.strftime("%d/%m/%y %H:%M:%S"), - messageType=message.message_type, - )) - - return PmsFolioMessageInfo( - folioId=folio_id, + 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, + ) + ) + PmsMessageInfo = self.env.datamodels["pms.message.info"] + return PmsMessageInfo( + folioMessages=folio_messages, reservationMessages=reservation_messages, ) def parse_message_body(self, message): - message_body = '' + 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: - # changed_field = str(tracking_value.changed_field) or "" - # old_value = str(tracking_value.old_value) or "" - # new_value = str(tracking_value.new_value) or "" - message_body += str(tracking_value)+' falta changed_field, old_value e new_value' + 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 From e14cd629c01d66761038b062b9d95ca39db948ed Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 13 Feb 2023 13:02:14 +0100 Subject: [PATCH 359/547] [IMP] pms_api_rest: add get service for invoices --- pms_api_rest/datamodels/pms_invoice.py | 15 ++- pms_api_rest/services/pms_invoice_service.py | 116 +++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_invoice.py b/pms_api_rest/datamodels/pms_invoice.py index 34cd6ab7c1..38c6523eb8 100644 --- a/pms_api_rest/datamodels/pms_invoice.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -3,6 +3,11 @@ 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) + name = fields.String(required=False, allow_none=True) class PmsAccountInvoiceInfo(Datamodel): _name = "pms.invoice.info" @@ -15,7 +20,8 @@ class PmsAccountInvoiceInfo(Datamodel): # 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")) + 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) @@ -27,3 +33,10 @@ class PmsAccountInvoiceInfo(Datamodel): # REVIEW: originDownPaymentId Only input field to service to # create downpayment invoices from payments originDownPaymentId = 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) + diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index f060e4bb9c..0bdfadb557 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -1,3 +1,5 @@ +from datetime import datetime + from odoo import _, fields from odoo.exceptions import UserError @@ -12,6 +14,120 @@ class PmsInvoiceService(Component): _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): + result_invoices = [] + + domain = [] + if pms_invoice_search_param.name: + domain.append(("name", "ilike", pms_invoice_search_param.name)) + + + 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([]) + amount_total = 100 + for invoice in self.env["account.move"].search( + domain, + order=pms_invoice_search_param.orderBy, + 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, + portalUrl=portal_url, + moveType=invoice.move_type, + isReversed=invoice.payment_state == "reversed", + isDownPaymentInvoice=invoice._is_downpayment(), + isSimplifiedInvoice=invoice.journal_id.is_simplified_invoice, + ) + ) + return PmsInvoiceResults( + invoices=result_invoices, + total=round(amount_total, 2), + totalInvoices=total_invoices + ) + @restapi.method( [ ( From d0e09d358e1fd6917235a5e071e11be4d790d229 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 16 Feb 2023 12:40:38 +0100 Subject: [PATCH 360/547] [IMP] pms_api_rest: add filters in invoices list --- pms_api_rest/datamodels/pms_invoice.py | 7 ++- pms_api_rest/services/pms_invoice_service.py | 51 ++++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/datamodels/pms_invoice.py b/pms_api_rest/datamodels/pms_invoice.py index 38c6523eb8..0d627ef78f 100644 --- a/pms_api_rest/datamodels/pms_invoice.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -7,7 +7,11 @@ class PmsInvoiceSearchParam(Datamodel): _name = "pms.invoice.search.param" _inherit = "pms.rest.metadata" id = fields.Integer(required=False, allow_none=True) - name = fields.String(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) class PmsAccountInvoiceInfo(Datamodel): _name = "pms.invoice.info" @@ -33,6 +37,7 @@ class PmsAccountInvoiceInfo(Datamodel): # 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) class PmsInvoiceResults(Datamodel): _name = "pms.invoice.results" diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 0bdfadb557..cc23afd542 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -6,6 +6,7 @@ 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.odoo.osv import expression class PmsInvoiceService(Component): @@ -31,14 +32,55 @@ def get_invoices(self, pms_invoice_search_param): result_invoices = [] domain = [] - if pms_invoice_search_param.name: - domain.append(("name", "ilike", pms_invoice_search_param.name)) + domain_fields = [ + ("state", "in", ("draft", "posted")) + ] + 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.paymentState: + 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([]) + total_invoices = self.env["account.move"].search_count(domain) amount_total = 100 for invoice in self.env["account.move"].search( domain, @@ -120,6 +162,9 @@ def get_invoices(self, pms_invoice_search_param): 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, ) ) return PmsInvoiceResults( From 358ab09ecabc20d5a56a73172c96b021fca3acb2 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 23 Feb 2023 12:34:03 +0100 Subject: [PATCH 361/547] [IMP] pms_api_rest: add amount total in invoices service --- pms_api_rest/services/pms_invoice_service.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index cc23afd542..9c7db054c4 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -33,7 +33,8 @@ def get_invoices(self, pms_invoice_search_param): domain = [] domain_fields = [ - ("state", "in", ("draft", "posted")) + ("state", "in", ("draft", "posted")), + ("move_type","=","out_invoice") ] domain_filter = list() @@ -81,7 +82,12 @@ def get_invoices(self, pms_invoice_search_param): PmsInvoiceInfo = self.env.datamodels["pms.invoice.info"] PmsInvoiceLineInfo = self.env.datamodels["pms.invoice.line.info"] total_invoices = self.env["account.move"].search_count(domain) - amount_total = 100 + amount_total = sum(self.env["account.move"].search( + domain, + order=pms_invoice_search_param.orderBy, + 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=pms_invoice_search_param.orderBy, From 0db5130e4b370f3f951674c4b6e0c846fa083adf Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 6 Mar 2023 17:51:34 +0100 Subject: [PATCH 362/547] [IMP] pms_api_rest: change domain and filter in get invoices and add ref field in invoice datamodel --- pms_api_rest/datamodels/pms_invoice.py | 1 + pms_api_rest/services/pms_invoice_service.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/datamodels/pms_invoice.py b/pms_api_rest/datamodels/pms_invoice.py index 0d627ef78f..defeb41cd2 100644 --- a/pms_api_rest/datamodels/pms_invoice.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -38,6 +38,7 @@ class PmsAccountInvoiceInfo(Datamodel): # 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) class PmsInvoiceResults(Datamodel): _name = "pms.invoice.results" diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 9c7db054c4..b7554fbced 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -34,7 +34,8 @@ def get_invoices(self, pms_invoice_search_param): domain = [] domain_fields = [ ("state", "in", ("draft", "posted")), - ("move_type","=","out_invoice") + ("move_type", "in", ("out_invoice", "out_refund")), + ("folio_ids", "!=", False) ] domain_filter = list() @@ -42,7 +43,15 @@ def get_invoices(self, pms_invoice_search_param): domain_fields.append( ("origin_agency_id","=", pms_invoice_search_param.originAgencyId), ) - if pms_invoice_search_param.paymentState: + 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) ) @@ -52,8 +61,8 @@ def get_invoices(self, pms_invoice_search_param): date_to = fields.Date.from_string(pms_invoice_search_param.dateEnd) domain_fields.extend( [ - ("invoice_date",">=", date_from), - ("invoice_date","<=",date_to), + ("invoice_date", ">=", date_from), + ("invoice_date", "<=", date_to), ] ) if pms_invoice_search_param.filter: @@ -171,6 +180,7 @@ def get_invoices(self, pms_invoice_search_param): originAgencyId=invoice.origin_agency_id.id if invoice.origin_agency_id else None, + ref=invoice.ref, ) ) return PmsInvoiceResults( From 75f618d06e7994329665bce827531ae80bcea4fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 6 Mar 2023 19:04:57 +0100 Subject: [PATCH 363/547] [FIX]pms_api_rest: fix import odoo.odoo --- pms_api_rest/services/pms_invoice_service.py | 67 +++++++++----------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index b7554fbced..e1909fef0e 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -2,11 +2,11 @@ from odoo import _, fields from odoo.exceptions import 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 -from odoo.odoo.osv import expression class PmsInvoiceService(Component): @@ -35,28 +35,25 @@ def get_invoices(self, pms_invoice_search_param): domain_fields = [ ("state", "in", ("draft", "posted")), ("move_type", "in", ("out_invoice", "out_refund")), - ("folio_ids", "!=", False) + ("folio_ids", "!=", False), ] domain_filter = list() if pms_invoice_search_param.originAgencyId: domain_fields.append( - ("origin_agency_id","=", pms_invoice_search_param.originAgencyId), + ("origin_agency_id", "=", pms_invoice_search_param.originAgencyId), ) 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")) - ) + 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): + 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( @@ -91,12 +88,16 @@ def get_invoices(self, pms_invoice_search_param): PmsInvoiceInfo = self.env.datamodels["pms.invoice.info"] PmsInvoiceLineInfo = self.env.datamodels["pms.invoice.line.info"] total_invoices = self.env["account.move"].search_count(domain) - amount_total = sum(self.env["account.move"].search( - domain, - order=pms_invoice_search_param.orderBy, - limit=pms_invoice_search_param.limit, - offset=pms_invoice_search_param.offset, - ).mapped("amount_total")) + amount_total = sum( + self.env["account.move"] + .search( + domain, + order=pms_invoice_search_param.orderBy, + 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=pms_invoice_search_param.orderBy, @@ -111,18 +112,12 @@ def get_invoices(self, pms_invoice_search_param): 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, + 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, + 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, @@ -133,16 +128,14 @@ def get_invoices(self, pms_invoice_search_param): ) ) 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 - ) + 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" @@ -167,10 +160,8 @@ def get_invoices(self, pms_invoice_search_param): 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, + 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, portalUrl=portal_url, moveType=invoice.move_type, @@ -186,7 +177,7 @@ def get_invoices(self, pms_invoice_search_param): return PmsInvoiceResults( invoices=result_invoices, total=round(amount_total, 2), - totalInvoices=total_invoices + totalInvoices=total_invoices, ) @restapi.method( From c0619648bf1c45dd0f6dca45c87142ec6b2cc73f Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 8 Mar 2023 09:35:38 +0100 Subject: [PATCH 364/547] [IMP] pms_api_rest: add property on domain in get_invoices --- pms_api_rest/datamodels/pms_invoice.py | 2 ++ pms_api_rest/services/pms_invoice_service.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_invoice.py b/pms_api_rest/datamodels/pms_invoice.py index defeb41cd2..d3ea667ef2 100644 --- a/pms_api_rest/datamodels/pms_invoice.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -12,6 +12,7 @@ class PmsInvoiceSearchParam(Datamodel): 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" @@ -39,6 +40,7 @@ class PmsAccountInvoiceInfo(Datamodel): 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" diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index e1909fef0e..d378a1fd91 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -43,6 +43,8 @@ def get_invoices(self, pms_invoice_search_param): 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")) @@ -83,7 +85,6 @@ def get_invoices(self, pms_invoice_search_param): 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"] @@ -171,7 +172,8 @@ def get_invoices(self, pms_invoice_search_param): originAgencyId=invoice.origin_agency_id.id if invoice.origin_agency_id else None, - ref=invoice.ref, + ref=invoice.ref if invoice.ref else None, + pmsPropertyId=invoice.pms_property_id if invoice.pms_property_id else None, ) ) return PmsInvoiceResults( From 165da673e02d2ea4836105552e34d9e974c4a57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 21 Mar 2023 16:45:51 +0100 Subject: [PATCH 365/547] [IMP]pms_api_rest: use active_test context to avoid get archived rooms --- .../services/pms_availability_plan_service.py | 20 +++++++++++-------- pms_api_rest/services/pms_calendar_service.py | 4 +++- .../services/pms_pricelist_service.py | 8 ++++++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index d1b20f9540..7f6370c29b 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -95,14 +95,18 @@ def get_availability_plan_rules( if not record_availability_plan_id: raise MissingError - rooms = self.env["pms.room"].search( - [ - ( - "pms_property_id", - "=", - availability_plan_rule_search_param.pmsPropertyId, - ) - ] + 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 = [ diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 9d137f7ffd..8924ef4562 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -332,7 +332,9 @@ def get_free_rooms(self, pms_calendar_search_param): { "room_type_id": room_type.id, "rooms_total": len( - self.env["pms.room"].search( + self.env["pms.room"] + .with_context(active_test=True) + .search( [ ("room_type_id", "=", room_type.id), ("pms_property_id", "=", pms_property_id), diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 3fa38af657..dd5d43ea6d 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -109,8 +109,12 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): ) if not record_pricelist: raise MissingError - rooms = self.env["pms.room"].search( - [("pms_property_id", "=", pricelist_item_search_param.pmsPropertyId)] + 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 = [] From 42f72898fecef053b7ac934d28083e4b3b583f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 21 Mar 2023 16:46:41 +0100 Subject: [PATCH 366/547] [IMP]pms_api_rest: use author name first (mail if not name) in messages services --- pms_api_rest/services/pms_folio_service.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index aca0197aae..26391e20dd 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1085,9 +1085,9 @@ def get_folio_reservation_messages(self, folio_id): reservation_messages.append( PmsReservationMessageInfo( reservationId=message.res_id, - author=message.email_from - if message.email_from - else message.author_id[0], + author=message.author_id.name + if message.author_id + else message.email_from, message=message_body, subject=subject, date=message.date.strftime("%d/%m/%y %H:%M:%S"), @@ -1108,9 +1108,9 @@ def get_folio_reservation_messages(self, folio_id): subject = folio_message.subject if folio_message.subject else None folio_messages.append( PmsFolioMessageInfo( - author=folio_message.email_from - if folio_message.email_from - else folio_message.author_id[1], + author=message.author_id.name + if message.author_id + else message.email_from, message=message_body, subject=subject, date=folio_message.date.strftime("%d/%m/%y %H:%M:%S"), From 6ee9a6f588c15732120bc57b0eed2e0041abe994 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 23 Mar 2023 17:27:35 +0100 Subject: [PATCH 367/547] [IMP] pms_api_rest: add language in folio services --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index d920a43f3c..ead3bd721a 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -44,6 +44,7 @@ class PmsFolioInfo(Datamodel): confirmReservations = fields.Boolean(required=False, allow_none=True) invoiceStatus = fields.String(required=False, allow_none=True) portalUrl = fields.String(required=False, allow_none=True) + language = fields.String(required=False, allow_none=True) class PmsFolioShortInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 26391e20dd..a417f90024 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -72,6 +72,7 @@ def get_folio(self, folio_id): if folio.out_service_description else None, portalUrl=portal_url, + language=folio.lang if folio.lang else None, ) else: raise MissingError(_("Folio not found")) @@ -465,6 +466,7 @@ def create_folio(self, pms_folio_info): "out_service_description": pms_folio_info.outOfServiceDescription if pms_folio_info.outOfServiceDescription else None, + "lang": pms_folio_info.language, } else: vals = { @@ -475,6 +477,7 @@ def create_folio(self, pms_folio_info): else False, "reservation_type": pms_folio_info.reservationType, "internal_comment": pms_folio_info.internalComment, + "lang": pms_folio_info.language, } if pms_folio_info.partnerId: vals.update( From d70fa0d1fb5ff0daf56df7c7bd7ec5f6b898ea2c Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 24 Mar 2023 15:10:32 +0100 Subject: [PATCH 368/547] [IMP] pms_api_rest: add language in property services --- pms_api_rest/datamodels/pms_property.py | 1 + pms_api_rest/services/pms_property_service.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index 97e280ef7c..df574573a5 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -28,3 +28,4 @@ class PmsPropertyInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 0684b17b68..c1acafe74a 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -47,6 +47,7 @@ def get_properties(self): simpleOutColor=prop.simple_out_color, simpleInColor=prop.simple_in_color, simpleFutureColor=prop.simple_future_color, + language=prop.lang, ) ) return result_properties @@ -85,6 +86,7 @@ def get_property(self, property_id): staffReservationColor=pms_property.staff_reservation_color, toAssignReservationColor=pms_property.to_assign_reservation_color, pendingPaymentReservationColor=pms_property.pending_payment_reservation_color, + language=pms_property.lang, ) return res From a9a41743475358ed75f3b43e379081ed8b3ed29d Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 27 Mar 2023 16:00:55 +0200 Subject: [PATCH 369/547] [IMP] pms_api_rest: add language in folio patch --- pms_api_rest/services/pms_folio_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index a417f90024..eb825c85aa 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -598,6 +598,8 @@ def update_folio(self, folio_id, pms_folio_info): 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 = { From 89dc392e6fbae5dafb8661c03eb774b242a42c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 24 Dec 2022 16:27:44 +0100 Subject: [PATCH 370/547] [ADD]pms_api_rest: avails service and datamodel --- pms_api_rest/datamodels/pms_avail.py | 20 +++++++ pms_api_rest/services/pms_avail_service.py | 65 ++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_avail.py create mode 100644 pms_api_rest/services/pms_avail_service.py 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/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 From d9db33b0f1d97da0e96a941df6023ec9b0b9bdfa Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 30 Dec 2022 13:18:21 +0100 Subject: [PATCH 371/547] [FIX] pms-api_rest: add avail datamodel & service to inits --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/services/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index c6619b677b..4f21569428 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -58,3 +58,4 @@ from . import pms_mail from . import pms_notification from . import pms_reservation_message +from . import pms_avail diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index bf7d290a4f..df359049c9 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -40,3 +40,4 @@ from . import pms_account_journal_service from . import pms_invoice_service from . import pms_notification_service +from . import pms_avail_service From 17f1ca7fa0a47a45852245061f2aaea9004ef5de Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 11 Jan 2023 19:22:28 +0100 Subject: [PATCH 372/547] [IMP] pms_api_rest: board service id @ service's service (datamodel & service) --- pms_api_rest/datamodels/pms_board_service.py | 1 + pms_api_rest/services/pms_board_service_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_board_service.py b/pms_api_rest/datamodels/pms_board_service.py index 5e8b8138b2..3fa85cf116 100644 --- a/pms_api_rest/datamodels/pms_board_service.py +++ b/pms_api_rest/datamodels/pms_board_service.py @@ -16,3 +16,4 @@ class PmsBoardServiceInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index 40c2b9dde8..2e52cdf774 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -51,6 +51,7 @@ def get_board_services(self, board_services_search_param): 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, ) ) return result_board_services From 6f62def2f5407d21b0ec9b2fc4649630ac2038d2 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 11 Jan 2023 19:22:37 +0100 Subject: [PATCH 373/547] [IMP] pms_api_rest: children 0 if None @ folio & reservation service --- pms_api_rest/services/pms_folio_service.py | 2 +- pms_api_rest/services/pms_reservation_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index eb825c85aa..e178db75fe 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -427,7 +427,7 @@ def get_folio_reservations(self, folio_id): )[reservation.state], children=reservation.children if reservation.children - else None, + else 0, readyForCheckin=reservation.ready_for_checkin, allowedCheckout=reservation.allowed_checkout, isSplitted=reservation.splitted, diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index bf5de43528..6230ca755b 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -96,7 +96,7 @@ def get_reservation(self, reservation_id, pms_search_param): stateDescription=dict( reservation.fields_get(["state"])["state"]["selection"] )[reservation.state], - children=reservation.children if reservation.children else None, + children=reservation.children if reservation.children else 0, readyForCheckin=reservation.ready_for_checkin, allowedCheckout=reservation.allowed_checkout, isSplitted=reservation.splitted, From 284e301da9a518c62fe110b68429f37334ae9ebb Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 19 Jan 2023 19:35:55 +0100 Subject: [PATCH 374/547] [IMP] pms-api_rest: update checkin & checkout @ update reservation service --- pms_api_rest/services/pms_reservation_service.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 6230ca755b..ef8ff69b4f 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -139,10 +139,6 @@ def _create_vals_from_params(self, reservation_vals, reservation_data): reservation_vals.update( {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} ) - if reservation_data.partnerRequests is not None: - reservation_vals.update( - {"partner_requests": reservation_data.partnerRequests} - ) return reservation_vals @restapi.method( @@ -214,7 +210,7 @@ def update_reservation(self, reservation_id, reservation_data): reservation.room_type_id = reservation_data.roomTypeId reservation_vals = self._create_vals_from_params( - reservation_vals, reservation_data + reservation_vals, reservation_data, reservation_id, ) # TODO: this should be @ pms core if ( @@ -607,7 +603,6 @@ def get_reservations(self, pms_search_param): if pms_search_param.toAssign: domain.append(("to_assign", "=", True)) domain.append(("checkin", ">=", fields.Date.today())) - domain.append(("state", "!=", "cancel")) reservations = self.env["pms.reservation"].search(domain) PmsReservationInfo = self.env.datamodels["pms.reservation.info"] if not reservations: From 05d6ada4df82ec774fbeecf61dbf96a69e45bdd9 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 19 Jan 2023 19:39:35 +0100 Subject: [PATCH 375/547] [FIX] pms-api_rest: fix precommit --- pms_api_rest/services/pms_reservation_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index ef8ff69b4f..1b60c53345 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -210,7 +210,9 @@ def update_reservation(self, reservation_id, reservation_data): reservation.room_type_id = reservation_data.roomTypeId reservation_vals = self._create_vals_from_params( - reservation_vals, reservation_data, reservation_id, + reservation_vals, + reservation_data, + reservation_id, ) # TODO: this should be @ pms core if ( From a4e2d29b46097fc83318d29e3d22198466ce5a51 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 14 Mar 2023 12:16:00 +0000 Subject: [PATCH 376/547] [WIP] pms-api-rest: wip PATCH/POST services problem --- pms_api_rest/datamodels/pms_board_service.py | 1 + pms_api_rest/datamodels/pms_reservation.py | 3 + .../services/pms_board_service_service.py | 2 + pms_api_rest/services/pms_folio_service.py | 1 + pms_api_rest/services/pms_price_service.py | 24 +++++-- .../services/pms_reservation_service.py | 63 +++++++++++++++---- 6 files changed, 75 insertions(+), 19 deletions(-) diff --git a/pms_api_rest/datamodels/pms_board_service.py b/pms_api_rest/datamodels/pms_board_service.py index 3fa85cf116..87207aa895 100644 --- a/pms_api_rest/datamodels/pms_board_service.py +++ b/pms_api_rest/datamodels/pms_board_service.py @@ -17,3 +17,4 @@ class PmsBoardServiceInfo(Datamodel): 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)) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index f3318b5684..74c2ad3405 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -24,6 +24,8 @@ class PmsReservationShortInfo(Datamodel): 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) + class PmsReservationInfo(Datamodel): @@ -34,6 +36,7 @@ class PmsReservationInfo(Datamodel): folioSequence = 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) diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index 2e52cdf774..957628e167 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -52,6 +52,7 @@ def get_board_services(self, board_services_search_param): roomTypeId=board_service.pms_room_type_id.id, amount=round(board_service.amount, 2), boardServiceId=board_service.pms_board_service_id, + productIds=board_service.board_service_line_ids.mapped("product_id.id"), ) ) return result_board_services @@ -79,6 +80,7 @@ def get_board_service(self, 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), + productIds=board_service.board_service_line_ids.mapped("product_id.id"), ) else: raise MissingError(_("Board Service not found")) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index e178db75fe..ea6d9987d9 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -435,6 +435,7 @@ def get_folio_reservations(self, folio_id): 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 diff --git a/pms_api_rest/services/pms_price_service.py b/pms_api_rest/services/pms_price_service.py index 09f5d8af3e..4b56b61da8 100644 --- a/pms_api_rest/services/pms_price_service.py +++ b/pms_api_rest/services/pms_price_service.py @@ -33,19 +33,25 @@ def get_prices(self, prices_search_param): room_type = self.env["pms.room.type"].search( [("id", "=", prices_search_param.roomTypeId)] ) - if prices_search_param.productId: + elif prices_search_param.productId and prices_search_param.boardServiceId: product = self.env["product.product"].search( [("id", "=", prices_search_param.productId)] ) - if prices_search_param.boardServiceId: board_service = self.env["pms.board.service.room.type"].search( [("id", "=", prices_search_param.boardServiceId)] ) - if sum([var is not False for var in [product, room_type, board_service]]) != 1: + elif prices_search_param.productId: + product = self.env["product.product"].search( + [("id", "=", prices_search_param.productId)] + ) + elif prices_search_param.boardServiceId: + board_service = self.env["pms.board.service.room.type"].search( + [("id", "=", prices_search_param.boardServiceId)] + ) + else: raise MissingError( _( - "It is necessary to indicate one and only one product," - " board service or room type" + "Wrong input param" ) ) @@ -72,6 +78,7 @@ def get_prices(self, prices_search_param): partner_id=prices_search_param.partnerId, product_qty=prices_search_param.productQty, date_consumption=price_date, + product_id=product.id if product else False, ), 2, ), @@ -143,9 +150,14 @@ def _get_board_service_price( partner_id=False, product_qty=False, date_consumption=False, + product_id=False, ): price = 0 - for product in board_service.board_service_line_ids.mapped("product_id"): + if product_id: + products = self.env['product.product'].browse(product_id) + else: + products = board_service.board_service_line_ids.mapped("product_id") + for product in products: price += self._get_product_price( product=product, pms_property_id=pms_property_id, diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 1b60c53345..720f249220 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -120,7 +120,7 @@ def get_reservation(self, reservation_id, pms_search_param): ) return res - def _create_vals_from_params(self, reservation_vals, reservation_data): + 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} @@ -139,6 +139,12 @@ def _create_vals_from_params(self, reservation_vals, reservation_data): reservation_vals.update( {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} ) + if reservation_data.checkin: + reservation_vals.update({"checkin": reservation_data.checkin}) + if reservation_data.checkout: + reservation_vals.update({"checkout": reservation_data.checkout}) + + return reservation_vals @restapi.method( @@ -203,25 +209,56 @@ def update_reservation(self, reservation_id, reservation_data): reservation.confirm() if reservation_data.toCheckout is not None and reservation_data.toCheckout: reservation.action_reservation_checkout() - if ( - reservation_data.roomTypeId - and reservation.room_type_id.id != reservation_data.roomTypeId - ): - reservation.room_type_id = reservation_data.roomTypeId reservation_vals = self._create_vals_from_params( reservation_vals, reservation_data, reservation_id, ) - # TODO: this should be @ pms core - if ( - reservation_data.boardServiceId is not None - and reservation_data.boardServiceId != reservation.board_service_room_id - ): - reservation.service_ids.filtered(lambda x: x.is_board_service).unlink() + + 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: - reservation.write(reservation_vals) + if reservation_data.boardServices: + reservation.with_context(skip_compute_service_ids=True).write(reservation_vals) + else: + reservation.write(reservation_vals) + print(reservation.service_ids.mapped("name")) + def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): # Return dict witch reservation.lines values (only modified if line exist, From 4492a61fbc9143290a11973bc33823df06196864 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 3 Apr 2023 19:11:46 +0100 Subject: [PATCH 377/547] [IMP] pms-api-rest: skip generate bs lines --- pms_api_rest/services/pms_folio_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ea6d9987d9..07cd5c4be1 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -522,13 +522,13 @@ def create_folio(self, pms_folio_info): "children": reservation.children, "preconfirm": pms_folio_info.preconfirm, } - reservation_record = self.env["pms.reservation"].create(vals) + reservation_record = self.env["pms.reservation"].with_context(skip_compute_service_ids=True).create(vals) if reservation.services: for service in reservation.services: vals = { "product_id": service.productId, "reservation_id": reservation_record.id, - "is_board_service": False, + "is_board_service": service.isBoardService, "service_line_ids": [ ( 0, From 604493a45f6d07cac966b1fd681836e00629d27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 5 Apr 2023 12:03:21 +0200 Subject: [PATCH 378/547] [IMP]pms_api_rest: user validator back config in login service --- pms_api_rest/services/pms_login_service.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 75281b8750..3e7ca02df2 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -32,7 +32,6 @@ class PmsLoginService(Component): cors="*", ) def login(self, user): - user_record = ( self.env["res.users"].sudo().search([("login", "=", user.username)]) ) @@ -47,6 +46,11 @@ def login(self, user): 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( @@ -57,8 +61,8 @@ def login(self, user): "username": user.username, "password": user.password, }, - key="pms_secret_key_example", - algorithm=jwt.ALGORITHMS.HS256, + key=validator.secret_key, + algorithm=validator.secret_algorithm, ) avail_rule_names = [] for avail_field in user_record.availability_rule_field_ids: From 9b34582bd9bb6e793a3547f8ee28068c2ec6d03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 6 Apr 2023 09:38:13 +0200 Subject: [PATCH 379/547] [IMP]pms_api_rest: add context avoid_availability_check in swap reservations --- pms_api_rest/services/pms_calendar_service.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 8924ef4562..81cba0ec7f 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -189,8 +189,12 @@ def swap_reservation_slices(self, swap_info): reservation_line.flush() affected_line.flush() - reservation_line.room_id = swap_info.roomId - affected_line.room_id = old_room_id + 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 reservation_line.occupies_availability = True affected_line.occupies_availability = True From 8bb35e7ef04b865bd61bfc646272287d7de41565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 6 Apr 2023 10:35:34 +0200 Subject: [PATCH 380/547] [DEL] empty file init in root --- __init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 __init__.py diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From e29e143ef81e3001c0bc082b8c1f1caa251d3798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 19 Apr 2023 19:02:08 +0200 Subject: [PATCH 381/547] [IMP]pms_api_rest: swap reservations calendar workflow --- pms_api_rest/services/pms_calendar_service.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 81cba0ec7f..0623dfbc70 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -183,12 +183,6 @@ def swap_reservation_slices(self, swap_info): ("room_id", "=", swap_info.roomId), ] ) - reservation_line.occupies_availability = False - affected_line.occupies_availability = False - - reservation_line.flush() - affected_line.flush() - reservation_line.with_context( avoid_availability_check=True ).room_id = swap_info.roomId @@ -196,12 +190,6 @@ def swap_reservation_slices(self, swap_info): avoid_availability_check=True ).room_id = old_room_id - reservation_line.occupies_availability = True - affected_line.occupies_availability = True - - reservation_line._compute_occupies_availability() - affected_line._compute_occupies_availability() - @restapi.method( [ ( From 543918e279ef379bfd4e6a05f36348796045112f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 19 May 2023 09:40:47 +0200 Subject: [PATCH 382/547] [IMP]pms_api_rest: force compute default board service un POST reservation --- pms_api_rest/services/pms_folio_service.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 07cd5c4be1..cafde33817 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -522,7 +522,11 @@ def create_folio(self, pms_folio_info): "children": reservation.children, "preconfirm": pms_folio_info.preconfirm, } - reservation_record = self.env["pms.reservation"].with_context(skip_compute_service_ids=True).create(vals) + reservation_record = ( + self.env["pms.reservation"] + .with_context(skip_compute_service_ids=True) + .create(vals) + ) if reservation.services: for service in reservation.services: vals = { @@ -544,6 +548,10 @@ def create_folio(self, pms_folio_info): ], } self.env["pms.service"].create(vals) + # 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: + reservation_record._compute_board_service_room_id() # REVIEW: analyze how to integrate the sending of mails from the API # with the configuration of the automatic mails pms # & From 64fca8e89c54bdab2ecfa7dab64b3e3532ad145e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 19 Jun 2023 13:53:11 +0200 Subject: [PATCH 383/547] [IMP]pms_api_rest: Set channel manager api servicer base --- pms_api_rest/services/pms_folio_service.py | 88 ++++++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index cafde33817..3a65780a1d 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -459,6 +459,7 @@ def get_folio_reservations(self, folio_id): auth="jwt_api_pms", ) def create_folio(self, pms_folio_info): + call_type = self.get_api_client_type() if pms_folio_info.reservationType == "out": vals = { "pms_property_id": pms_folio_info.pmsPropertyId, @@ -467,19 +468,21 @@ def create_folio(self, pms_folio_info): "out_service_description": pms_folio_info.outOfServiceDescription if pms_folio_info.outOfServiceDescription else None, - "lang": pms_folio_info.language, } else: vals = { "pms_property_id": pms_folio_info.pmsPropertyId, - "sale_channel_origin_id": pms_folio_info.saleChannelId, "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, "internal_comment": pms_folio_info.internalComment, - "lang": pms_folio_info.language, + "lang": self.get_language(pms_folio_info.language), } + if pms_folio_info.partnerId: vals.update( { @@ -515,7 +518,10 @@ def create_folio(self, pms_folio_info): "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, + "board_service_room_id": self.get_board_service_room_type_id( + reservation.boardServiceId, + reservation.roomTypeId, + ), "preferred_room_id": reservation.preferredRoomId, "adults": reservation.adults, "reservation_type": pms_folio_info.reservationType, @@ -524,7 +530,10 @@ def create_folio(self, pms_folio_info): } reservation_record = ( self.env["pms.reservation"] - .with_context(skip_compute_service_ids=True) + .with_context( + skip_compute_service_ids=True, + force_overbooking=True if call_type == "external_app" else False, + ) .create(vals) ) if reservation.services: @@ -1181,3 +1190,72 @@ def parse_message_body(self, message): + str(new_value) ) return message_body + + def get_api_client_type(self): + """ + Returns the type of the call: + - Internal APP: The call is made from the internal vue app + - External APP: The call is made from an external app + """ + # TODO: Set the new roles in API Key users: + # - Channel Manager + # - Booking Engine + # - ... + if "neobookings" in self.env.user.login: + return "external_app" + return "internal_app" + + 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) + """ + if sale_channel_id: + return sale_channel_id + if not agency_id and self.get_api_client_type() == "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["pms.agency"].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 + """ + if self.get_api_client_type() == "internal_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): + """ + 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 + """ + if self.get_api_client_type() == "internal_app": + return board_service_id + board_service = self.env["pms.board.service"].browse(board_service_id) + room_type = self.env["pms.room.type"].browse(room_type_id) + if board_service and room_type: + return ( + self.env["pms.board.service.room.type"] + .search( + [ + ("board_service_id", "=", board_service.id), + ("pms_room_type_id", "=", room_type.id), + ], + limit=1, + ) + .id + ) + return False From be5722ccd71747288d0f527b24706b4dc0f3cd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 26 Jun 2023 09:36:49 +0200 Subject: [PATCH 384/547] [IMP]pms_api_rest: Prepare board_servide folio post to external clients --- pms_api_rest/services/pms_folio_service.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 3a65780a1d..5a724515f1 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -521,6 +521,7 @@ def create_folio(self, pms_folio_info): "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, @@ -1236,7 +1237,9 @@ def get_language(self, lang_code): 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): + 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. @@ -1251,8 +1254,9 @@ def get_board_service_room_type_id(self, board_service_id, room_type_id): self.env["pms.board.service.room.type"] .search( [ - ("board_service_id", "=", board_service.id), + ("pms_board_service_id", "=", board_service.id), ("pms_room_type_id", "=", room_type.id), + ("pms_property_id", "=", pms_property_id), ], limit=1, ) From 42084031de02c7cbc8c977f31ccce4663486022e Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 19 Apr 2023 12:58:29 +0200 Subject: [PATCH 385/547] [IMP]pms_api_rest: added reselling field in reservation line datamodel, GET and PATCH --- pms_api_rest/datamodels/pms_calendar.py | 1 + pms_api_rest/datamodels/pms_reservation_line.py | 1 + pms_api_rest/services/pms_calendar_service.py | 2 ++ pms_api_rest/services/pms_reservation_line_service.py | 3 +++ pms_api_rest/services/pms_reservation_service.py | 1 + 5 files changed, 8 insertions(+) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 7b7483fadb..6b9e76c8b5 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -69,6 +69,7 @@ class PmsCalendarInfo(Datamodel): 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 PmsCalendarAlertsPerDay(Datamodel): diff --git a/pms_api_rest/datamodels/pms_reservation_line.py b/pms_api_rest/datamodels/pms_reservation_line.py index 1deeb03b20..f7597b6844 100644 --- a/pms_api_rest/datamodels/pms_reservation_line.py +++ b/pms_api_rest/datamodels/pms_reservation_line.py @@ -22,3 +22,4 @@ class PmsReservationLineInfo(Datamodel): roomId = fields.Integer(required=False, allow_none=False) reservationId = fields.Integer(required=False, allow_none=False) pmsPropertyId = fields.Integer(required=False, allow_none=False) + isReselling = fields.Boolean(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 0623dfbc70..7686e02aae 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -59,6 +59,7 @@ def get_calendar(self, calendar_search_param): "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()) @@ -152,6 +153,7 @@ def get_calendar(self, calendar_search_param): nextLineSplitted=next_line_splitted, previousLineSplitted=previous_line_splitted, closureReasonId=line["closure_reason_id"], + isReselling=line["is_reselling"], ) ) return result_lines diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index 2ea1695bde..5c5c0756e6 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -43,6 +43,7 @@ def get_reservation_line(self, reservation_line_id): 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")) @@ -126,6 +127,8 @@ def update_reservation_line(self, reservation_line_id, reservation_line_info): 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 index 720f249220..98864b306c 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -321,6 +321,7 @@ def get_reservation_line(self, reservation_id): 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 From 5b988044452db0aebeea2adf1e7b4e3a228c5187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 2 Jul 2023 09:04:06 +0200 Subject: [PATCH 386/547] [IMP]pms_api_rest: Avoid set number invoice updated draft invoices --- pms_api_rest/services/pms_invoice_service.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index d378a1fd91..196f18d298 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -44,7 +44,9 @@ def get_invoices(self, pms_invoice_search_param): ("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)) + 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")) @@ -173,7 +175,9 @@ def get_invoices(self, pms_invoice_search_param): if invoice.origin_agency_id else None, ref=invoice.ref if invoice.ref else None, - pmsPropertyId=invoice.pms_property_id if invoice.pms_property_id else None, + pmsPropertyId=invoice.pms_property_id + if invoice.pms_property_id + else None, ) ) return PmsInvoiceResults( @@ -332,6 +336,9 @@ def _direct_move_update(self, invoice, new_vals): 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 From 4c36f83d7776db2d590ff81ebf78a65814d27b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 11 Jul 2023 11:09:05 +0200 Subject: [PATCH 387/547] [WIP]pms_api_rest: external clients API --- pms_api_rest/datamodels/pms_reservation.py | 6 +- pms_api_rest/datamodels/pms_service.py | 1 + pms_api_rest/models/product_product.py | 11 + pms_api_rest/services/pms_folio_service.py | 256 ++++++++++++++++-- pms_api_rest/views/product_template_views.xml | 14 + 5 files changed, 264 insertions(+), 24 deletions(-) create mode 100644 pms_api_rest/models/product_product.py create mode 100644 pms_api_rest/views/product_template_views.xml diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 74c2ad3405..709a157168 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -27,7 +27,6 @@ class PmsReservationShortInfo(Datamodel): pricelistId = fields.Integer(required=False, allow_none=True) - class PmsReservationInfo(Datamodel): _name = "pms.reservation.info" id = fields.Integer(required=False, allow_none=True) @@ -36,7 +35,9 @@ class PmsReservationInfo(Datamodel): folioSequence = 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) + 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) @@ -77,6 +78,7 @@ class PmsReservationInfo(Datamodel): NestedModel("pms.service.info"), required=False, allow_none=True ) partnerRequests = fields.String(required=False, allow_none=True) + nights = fields.Integer(required=False, allow_none=True) # TODO: Refact # messages = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py index ef0e662132..004b72dffb 100644 --- a/pms_api_rest/datamodels/pms_service.py +++ b/pms_api_rest/datamodels/pms_service.py @@ -17,3 +17,4 @@ class PmsServiceInfo(Datamodel): 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) diff --git a/pms_api_rest/models/product_product.py b/pms_api_rest/models/product_product.py new file mode 100644 index 0000000000..57c30338a5 --- /dev/null +++ b/pms_api_rest/models/product_product.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + 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/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 5a724515f1..a7101042a7 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1,4 +1,5 @@ import base64 +import logging from datetime import datetime, timedelta from odoo import _, fields @@ -10,6 +11,8 @@ from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component +_logger = logging.getLogger(__name__) + class PmsFolioService(Component): _inherit = "base.rest.service" @@ -330,7 +333,7 @@ def create_folio_charge(self, folio_id, pms_account_payment_info): reservations=reservations, services=False, partner=partner_id, - date=datetime.strptime(pms_account_payment_info.date, "%m/%d/%Y"), + 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" @@ -478,7 +481,8 @@ def create_folio(self, pms_folio_info): "sale_channel_origin_id": self.get_channel_origin_id( pms_folio_info.saleChannelId, pms_folio_info.agencyId ), - "reservation_type": pms_folio_info.reservationType, + "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), } @@ -517,7 +521,7 @@ def create_folio(self, pms_folio_info): "checkout": reservation.checkout, "pms_property_id": pms_folio_info.pmsPropertyId, "pricelist_id": pms_folio_info.pricelistId, - "external_reference": pms_folio_info.externalReference, + "external_reference": pms_folio_info.externalReference or "normal", "board_service_room_id": self.get_board_service_room_type_id( reservation.boardServiceId, reservation.roomTypeId, @@ -525,7 +529,7 @@ def create_folio(self, pms_folio_info): ), "preferred_room_id": reservation.preferredRoomId, "adults": reservation.adults, - "reservation_type": pms_folio_info.reservationType, + "reservation_type": pms_folio_info.reservationType or "normal", "children": reservation.children, "preconfirm": pms_folio_info.preconfirm, } @@ -539,25 +543,40 @@ def create_folio(self, pms_folio_info): ) if reservation.services: for service in reservation.services: - vals = { - "product_id": service.productId, - "reservation_id": reservation_record.id, - "is_board_service": service.isBoardService, - "service_line_ids": [ - ( - 0, - False, + 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( { - "date": line.date, - "price_unit": line.priceUnit, - "discount": line.discount or 0, - "day_qty": line.quantity, - }, + "product_qty": service.quantity, + } ) - for line in service.serviceLines - ], - } - self.env["pms.service"].create(vals) + self.env["pms.service"].create(vals) # 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: @@ -1224,7 +1243,7 @@ def get_channel_origin_id(self, sale_channel_id, agency_id): ) .id ) - agency = self.env["pms.agency"].browse(agency_id) + agency = self.env["res.partner"].browse(agency_id) if agency: return agency.sale_channel_id.id return False @@ -1263,3 +1282,196 @@ def get_board_service_room_type_id( .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): + folio = self.env["pms.folio"].search( + [ + ("external_reference", "=", 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) + return folio.id + + @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): + folio = self.env["pms.folio"].browse(folio_id) + if not folio: + raise MissingError(_("Folio not found")) + self.update_folio_values(folio, pms_folio_info) + return folio.id + + def update_folio_values(self, folio, pms_folio_info): + call_type = self.get_api_client_type() + folio_vals = {} + if pms_folio_info.state == "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.confirm() + if ( + pms_folio_info.internalComment is not None + and folio.internal_comment != pms_folio_info.internalComment + ): + folio_vals.update({"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) != pms_folio_info.language + ): + folio_vals.update({"lang": self.get_language(pms_folio_info.language)}) + if pms_folio_info.reservations: + reservations_vals = self.wrapper_reservations( + folio, pms_folio_info.reservations + ) + if reservations_vals: + folio_vals.update({"reservation_ids": reservations_vals}) + if folio_vals: + if reservations_vals: + folio.reservation_ids.filtered( + lambda r: r.state != "cancel" + ).with_context(modified=True, force_write_blocked=True).action_cancel() + folio.with_context( + skip_compute_service_ids=True, + force_overbooking=True if call_type == "external_app" else False, + ).write(folio_vals) + _logger.info("FOLIO VALS", folio_vals) + + 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 + """ + cmds = [] + for info_reservation in info_reservations: + vals = {} + vals.update({"folio_id": folio.id}) + if info_reservation.roomTypeId: + vals.update({"room_type_id": info_reservation.roomTypeId}) + if info_reservation.checkin: + vals.update({"checkin": info_reservation.checkin}) + if info_reservation.checkout: + vals.update({"checkout": info_reservation.checkout}) + if info_reservation.pricelistId: + vals.update({"pricelist_id": info_reservation.pricelistId}) + if info_reservation.boardServiceId: + vals.update( + { + "board_service_room_id": self.get_board_service_room_type_id( + info_reservation.boardServiceId, + info_reservation.roomTypeId, + folio.pms_property_id.id, + ) + } + ) + if info_reservation.preferredRoomId: + vals.update({"preferred_room_id": info_reservation.preferredRoomId}) + if info_reservation.adults: + vals.update({"adults": info_reservation.adults}) + if info_reservation.children: + vals.update({"children": info_reservation.children}) + if info_reservation.reservationLines: + reservation_lines_cmds = self.wrapper_reservation_lines( + info_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_reservation.services + ) + if reservation_services_cmds: + vals.update({"service_ids": reservation_services_cmds}) + if not vals: + continue + else: + cmds.append((0, False, vals)) + return cmds + + def wrapper_reservation_lines(self, reservation): + cmds = [] + for line in reservation.reservationLines: + cmds.append( + ( + 0, + False, + { + "date": line.date, + "price": line.price, + "discount": line.discount or 0, + }, + ) + ) + return cmds + + def wrapper_reservation_services(self, info_reservations): + cmds = [] + for service in info_reservations: + cmds.append( + ( + 0, + False, + { + "product_id": service.productId, + "product_qty": service.quantity, + "discount": service.discount or 0, + }, + ) + ) + return cmds 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 + + + + + + + + + From 9716f08378336bf4790693f1263d122d522f4d8c Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 18 Apr 2023 09:29:22 +0200 Subject: [PATCH 388/547] [IMP] pms_api_rest: number of unassigned reserv. service notification --- pms_api_rest/datamodels/pms_notification.py | 2 ++ .../services/pms_notification_service.py | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/pms_api_rest/datamodels/pms_notification.py b/pms_api_rest/datamodels/pms_notification.py index 9582c219c2..19b85e7e6a 100644 --- a/pms_api_rest/datamodels/pms_notification.py +++ b/pms_api_rest/datamodels/pms_notification.py @@ -6,6 +6,7 @@ class PmsNotificationSearch(Datamodel): _name = "pms.notification.search" fromTimestamp = fields.String(required=False) + pmsPropertyId = fields.Integer(required=False) class PmsNotificationInfo(Datamodel): @@ -16,3 +17,4 @@ class PmsNotificationInfo(Datamodel): 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/services/pms_notification_service.py b/pms_api_rest/services/pms_notification_service.py index 81af342deb..8f42a8118b 100644 --- a/pms_api_rest/services/pms_notification_service.py +++ b/pms_api_rest/services/pms_notification_service.py @@ -1,5 +1,6 @@ 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 @@ -11,6 +12,33 @@ class PmsNotificationService(Component): _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( + [ + ("pms_property_id", "=", pms_notification_search.pmsPropertyId), + ("checkin", ">=", fields.Date.today()), + ("to_assign", "=", True), + ], + ) + PmsNotificationInfo = self.env.datamodels["pms.notification.info"] + return PmsNotificationInfo( + numReservationsToAssign=num_reservation_ids_to_assign + ) + @restapi.method( [ ( From 35cf318487ca60f0f1916e47d3d8b022f5321023 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 25 Apr 2023 12:07:58 +0200 Subject: [PATCH 389/547] [IMP]pms_api_rest: added firstCheckin and createDate fields in folio info datamodel --- pms_api_rest/datamodels/pms_folio.py | 2 ++ pms_api_rest/services/pms_folio_service.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index ead3bd721a..5abaeeec09 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -24,7 +24,9 @@ class PmsFolioInfo(Datamodel): 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) pmsPropertyId = fields.Integer(required=False, allow_none=True) partnerId = fields.Integer(required=False, allow_none=True) reservations = fields.List( diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index a7101042a7..18b14b8591 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -57,7 +57,9 @@ def get_folio(self, folio_id): 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(), internalComment=folio.internal_comment if folio.internal_comment else None, From f655f85fa806a7293edef05c8d61349bdcdfc9f9 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 10 May 2023 09:32:19 +0200 Subject: [PATCH 390/547] [IMP]pms_api_rest: added defaultCode field in room type class datamodel --- pms_api_rest/datamodels/pms_room_type_class.py | 1 + pms_api_rest/services/pms_room_type_class_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_room_type_class.py b/pms_api_rest/datamodels/pms_room_type_class.py index 43bd8d401f..4498951d73 100644 --- a/pms_api_rest/datamodels/pms_room_type_class.py +++ b/pms_api_rest/datamodels/pms_room_type_class.py @@ -13,4 +13,5 @@ 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) diff --git a/pms_api_rest/services/pms_room_type_class_service.py b/pms_api_rest/services/pms_room_type_class_service.py index 8566d6d6e3..28dfa3ea0a 100644 --- a/pms_api_rest/services/pms_room_type_class_service.py +++ b/pms_api_rest/services/pms_room_type_class_service.py @@ -49,6 +49,7 @@ def get_room_type_class(self, room_type_class_search_param): PmsRoomTypeClassInfo( id=room.id, name=room.name, + defaultCode=room.default_code if room.default_code else None, pmsPropertyIds=room.pms_property_ids.mapped("id"), ) ) From 0bdc5555806eba7f17e5a50f969a6f79c8d9c539 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 27 Apr 2023 10:46:13 +0200 Subject: [PATCH 391/547] [IMP] pms_api_rest: add fields in get_folios --- pms_api_rest/datamodels/pms_folio.py | 8 ++++++++ pms_api_rest/services/pms_folio_service.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 5abaeeec09..2dc9646b3a 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -52,12 +52,20 @@ class PmsFolioInfo(Datamodel): 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)) 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) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 18b14b8591..9542efa3d8 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -198,13 +198,19 @@ def get_folios(self, folio_search_param): "checkout": datetime.combine( reservation.checkout, datetime.min.time() ).isoformat(), + "stateCode": reservation.state, "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, @@ -215,15 +221,22 @@ def get_folios(self, folio_search_param): if reservation.agency_id else None, "isSplitted": reservation.splitted, + "toAssign": reservation.to_assign, + "reservationType": reservation.reservation_type, } ) result_folios.append( PmsFolioShortInfo( id=folio.id, + name=folio.name, + state=dict(folio.fields_get(["state"])["state"]["selection"])[ + 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( @@ -233,6 +246,11 @@ def get_folios(self, folio_search_param): )[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), ) ) return result_folios From c9f8a9fc9aed297038a6a8fdf95654447ab5a77f Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 27 Apr 2023 19:38:10 +0200 Subject: [PATCH 392/547] [IMP]pms-api-rest: filter folio by state and limit/offset --- pms_api_rest/datamodels/__init__.py | 3 +- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/pms_folio_service.py | 34 +++++++++++++--------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 4f21569428..dc54f816ac 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -1,3 +1,4 @@ +from . import pms_rest_metadata from . import pms_calendar from . import pms_folio @@ -8,7 +9,7 @@ from . import pms_reservation from . import pms_reservation_line -from . import pms_rest_metadata + from . import pms_checkin_partner from . import pms_partner diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 2dc9646b3a..f94b8bd4b5 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -6,6 +6,7 @@ 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) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 9542efa3d8..cf9b6e5dc1 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -91,7 +91,7 @@ def get_folio(self, folio_id): "GET", ) ], - input_param=Datamodel("pms.folio.search.param"), + input_param=Datamodel("pms.folio.search.param", is_list=False), output_param=Datamodel("pms.folio.short.info", is_list=True), auth="jwt_api_pms", ) @@ -155,20 +155,23 @@ def get_folios(self, folio_search_param): [("reservation_type", "!=", "out")], ] domain_filter.append(expression.AND(subdomains)) - else: - subdomain_checkin = [ - [("state", "in", ("confirm", "arrival_delayed"))], - [("checkin", "<=", fields.Date.today())], - ] - subdomain_checkin = expression.AND(subdomain_checkin) - subdomain_checkout = [ + elif folio_search_param.filterByState == 'onBoard': + subdomains = [ [("state", "in", ("onboard", "departure_delayed"))], - [("checkout", "=", fields.Date.today())], + [("reservation_type", "!=", "out")], ] - subdomain_checkout = expression.AND(subdomain_checkout) - domain_filter.append( - expression.OR([subdomain_checkin, subdomain_checkout]) - ) + domain_filter.append(expression.AND(subdomains)) + elif folio_search_param.filterByState == 'toAssign': + subdomains = [ + [("to_assign", "=", True)], + [("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: @@ -185,7 +188,10 @@ def get_folios(self, folio_search_param): PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] for folio in self.env["pms.folio"].search( - [("id", "in", reservations_result)], order="write_date desc" + [("id", "in", reservations_result)], + order="write_date desc", + limit=folio_search_param.limit, + offset=folio_search_param.offset, ): reservations = [] for reservation in folio.reservation_ids: From ac00d326b574102b93b542901916f7eee0b43110 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 24 May 2023 16:44:36 +0200 Subject: [PATCH 393/547] [IMP] pms_api_rest: add fields in reservations on get_folios --- pms_api_rest/services/pms_folio_service.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index cf9b6e5dc1..624feec3f9 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -205,9 +205,11 @@ def get_folios(self, folio_search_param): 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, @@ -229,6 +231,10 @@ def get_folios(self, folio_search_param): "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, } ) result_folios.append( From 8784088b378f1f7264b73770f19e1a2d4db64d4b Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 25 May 2023 12:54:07 +0200 Subject: [PATCH 394/547] [IMP] pms_api_rest: filter out reservations in get_folios --- pms_api_rest/services/pms_folio_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 624feec3f9..0490edb45c 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -188,7 +188,7 @@ def get_folios(self, folio_search_param): PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] for folio in self.env["pms.folio"].search( - [("id", "in", reservations_result)], + [("id", "in", reservations_result),("reservation_type", "!=", "out")], order="write_date desc", limit=folio_search_param.limit, offset=folio_search_param.offset, From 2f7e56388cbb98aa0d05021271ecc2810c19900a Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 29 May 2023 12:08:42 +0200 Subject: [PATCH 395/547] [IMP] pms_api_rest: add fields in get_folio_reservations --- pms_api_rest/datamodels/pms_reservation.py | 3 +++ pms_api_rest/services/pms_folio_service.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 709a157168..06f2aa6879 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -11,6 +11,7 @@ class PmsReservationShortInfo(Datamodel): 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) @@ -25,6 +26,8 @@ class PmsReservationShortInfo(Datamodel): 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) class PmsReservationInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 0490edb45c..1e2f266556 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -451,6 +451,9 @@ def get_folio_reservations(self, folio_id): 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, @@ -476,6 +479,10 @@ def get_folio_reservations(self, folio_id): lambda x: not x.is_board_service ).mapped("product_qty") ), + nights= reservation.nights, + numServices= len(reservation.service_ids) + if reservation.service_ids + else 0, ) ) From 5c79a6d33ccb245ac0db46983185ccc67826e87c Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 29 May 2023 12:46:05 +0200 Subject: [PATCH 396/547] [IMP]pms_api_rest: added to_assign in fetch_folio_reservations --- pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/services/pms_folio_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 06f2aa6879..226c812a09 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -28,6 +28,7 @@ class PmsReservationShortInfo(Datamodel): 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) class PmsReservationInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 1e2f266556..b5b346de03 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -483,6 +483,7 @@ def get_folio_reservations(self, folio_id): numServices= len(reservation.service_ids) if reservation.service_ids else 0, + toAssign=reservation.to_assign, ) ) From 73303c721caa4470e1f69cbc8460ca41087989cf Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 29 May 2023 13:51:04 +0200 Subject: [PATCH 397/547] [IMP]pms-api_rest: added nights and numServices to reservation datamodel --- pms_api_rest/datamodels/pms_reservation.py | 3 +++ pms_api_rest/services/pms_folio_service.py | 2 ++ pms_api_rest/services/pms_reservation_service.py | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 226c812a09..2da39e73b9 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -29,6 +29,7 @@ class PmsReservationShortInfo(Datamodel): 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) class PmsReservationInfo(Datamodel): @@ -76,6 +77,8 @@ class PmsReservationInfo(Datamodel): commissionAmount = 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( diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index b5b346de03..7cf8013fb0 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -235,6 +235,7 @@ def get_folios(self, folio_search_param): "numServices": len(reservation.service_ids) if reservation.service_ids else 0, + "overbooking": reservation.overbooking, } ) result_folios.append( @@ -484,6 +485,7 @@ def get_folio_reservations(self, folio_id): if reservation.service_ids else 0, toAssign=reservation.to_assign, + overbooking=reservation.overbooking, ) ) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 98864b306c..63ea985a8b 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -117,7 +117,11 @@ def get_reservation(self, reservation_id, pms_search_param): partnerRequests=reservation.partner_requests if reservation.partner_requests else None, - ) + nights=reservation.nights, + numServices=len(reservation.service_ids) + if reservation.service_ids + else 0, + ) return res def _create_vals_from_params(self, reservation_vals, reservation_data, reservation_id): From 1dbf3fd729375e6ab44909a2b7288bf22a7e53ac Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 12 May 2023 20:50:21 +0200 Subject: [PATCH 398/547] [IMP]pms_api_rest: added user services and datamodel fields --- pms_api_rest/datamodels/pms_user.py | 10 ++- pms_api_rest/models/res_users.py | 8 ++ pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/pms_login_service.py | 7 +- pms_api_rest/services/pms_user_service.py | 100 +++++++++++++++++++++ 5 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 pms_api_rest/services/pms_user_service.py diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index 51e01ab3d2..9174f91976 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -7,17 +7,21 @@ 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) + userId = fields.Integer(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=True, allow_none=False) + expirationDate = fields.Integer(required=False, allow_none=True) userId = fields.Integer(required=True, allow_none=False) userName = fields.String(required=True, allow_none=False) + userEmail = fields.String(required=False, allow_none=True) + userPhone = fields.String(required=False, allow_none=True) userImageBase64 = fields.String(required=False, allow_none=True) - defaultPropertyId = fields.Integer(required=True, allow_none=False) - defaultPropertyName = fields.String(required=True, allow_none=False) + 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 ) diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index cf821d5448..c394fb4185 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -14,6 +14,14 @@ class ResUsers(models.Model): 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, + ) + def _get_default_avail_rule_fields(self): default_avail_rule_fields = self.env["ir.model.fields"].search( [ diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index df359049c9..cc14bf2003 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -41,3 +41,4 @@ from . import pms_invoice_service from . import pms_notification_service from . import pms_avail_service +from . import pms_user_service diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 3e7ca02df2..5e0a04a810 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -73,8 +73,13 @@ def login(self, user): expirationDate=timestamp_expire_in_a_min, userId=user_record.id, userName=user_record.name, + userEmail=user_record.email, + userPhone=user_record.phone if user_record.phone else None, defaultPropertyId=user_record.pms_property_id.id, defaultPropertyName=user_record.pms_property_id.name, - userImageBase64=user_record.partner_id.image_1024 or None, + userImageBase64=user_record.partner_id.image_1024 + if user_record.partner_id.image_1024 + else None, + isNewInterfaceUser=user_record.is_new_interface_app_user, availabilityRuleFields=avail_rule_names, ) 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..62588c35cd --- /dev/null +++ b/pms_api_rest/services/pms_user_service.py @@ -0,0 +1,100 @@ +import base64 +import tempfile +import os +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 import _ +from odoo.odoo.exceptions import MissingError + + +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, + userEmail=user.email if user.email else "", + userPhone=user.phone if user.phone else "", + userImageBase64=user.image_1920 if user.image_1920 else "", + 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: + user.write( + { + "name": input_data.userName, + "email": input_data.userEmail, + "phone": str(input_data.userPhone), + "is_new_interface_app_user": input_data.isNewInterfaceUser, + } + ) + if input_data.userImageBase64: + 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), + "image_128": base64.b64encode(user_image), + "image_256": base64.b64encode(user_image), + "image_1920": base64.b64encode(user_image), + "image_512": base64.b64encode(user_image), + } + ) + else: + user.write( + { + "image_1024": '', + "image_128": '', + "image_256": '', + "image_1920": '', + "image_512": '', + } + ) + return True + From f40041f19c01568257bde8c076908f0c9a447f22 Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 15 May 2023 18:30:11 +0200 Subject: [PATCH 399/547] [REF]pms_api_rest: is_new_interface_app_user change in write_user --- pms_api_rest/services/pms_user_service.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index 62588c35cd..533b47d631 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -60,15 +60,20 @@ def get_user(self, user_id): 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": str(input_data.userPhone), - "is_new_interface_app_user": input_data.isNewInterfaceUser, + "phone": input_data.userPhone, } ) - if input_data.userImageBase64: + 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 @@ -80,20 +85,12 @@ def write_user(self, user_id, input_data): user.write( { "image_1024": base64.b64encode(user_image), - "image_128": base64.b64encode(user_image), - "image_256": base64.b64encode(user_image), - "image_1920": base64.b64encode(user_image), - "image_512": base64.b64encode(user_image), } ) else: user.write( { "image_1024": '', - "image_128": '', - "image_256": '', - "image_1920": '', - "image_512": '', } ) return True From 7520cf6895735f70bc1b1cc724ffaa22b38dec8a Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 16 May 2023 21:39:33 +0200 Subject: [PATCH 400/547] [IMP]pms_api_rest: added service to change userr password --- pms_api_rest/datamodels/pms_user.py | 5 ++++ pms_api_rest/services/pms_user_service.py | 29 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index 9174f91976..eda4427130 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -7,6 +7,7 @@ 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) @@ -25,3 +26,7 @@ class PmsApiRestUserOutput(Datamodel): 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/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index 533b47d631..5979eea09b 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -95,3 +95,32 @@ def write_user(self, user_id, input_data): ) 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: + raise MissingError(_("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, + ) + From 6dee5740931bb5baee00e68afeb98e9f0ae89955 Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 22 May 2023 20:34:11 +0200 Subject: [PATCH 401/547] [IMP]pms_api_rest: added services to reset user password --- pms/__manifest__.py | 1 + pms/data/pms_reset_password_email.xml | 109 ++++++++++++++++++++++ pms_api_rest/__manifest__.py | 1 + pms_api_rest/datamodels/pms_user.py | 2 + pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/res_company.py | 7 ++ pms_api_rest/services/pms_user_service.py | 77 ++++++++++++++- pms_api_rest/views/res_company_views.xml | 12 +++ 8 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 pms/data/pms_reset_password_email.xml create mode 100644 pms_api_rest/models/res_company.py create mode 100644 pms_api_rest/views/res_company_views.xml diff --git a/pms/__manifest__.py b/pms/__manifest__.py index 8149126c27..8ea4363e46 100644 --- a/pms/__manifest__.py +++ b/pms/__manifest__.py @@ -42,6 +42,7 @@ "data/pms_precheckin_invitation_email_template.xml", "data/pms_data.xml", "data/traveller_report_paperformat.xml", + "data/pms_reset_password_email.xml", "report/pms_folio.xml", "report/pms_folio_templates.xml", "report/traveller_report_action.xml", diff --git a/pms/data/pms_reset_password_email.xml b/pms/data/pms_reset_password_email.xml new file mode 100644 index 0000000000..d54926c564 --- /dev/null +++ b/pms/data/pms_reset_password_email.xml @@ -0,0 +1,109 @@ + + + + + Pms Reset Password + + Restablecer Contraseña + "${object.company_id.name | safe}" <${(object.company_id.email or user.email) | safe}> + ${object.email_formatted | safe} + + + + +
+ + + + + + + + + + + + + + + +
+ + + +
+ Tu Cuenta en Roomdoo
+ + ${object.name} + +
+ ${object.company_id.name} +
+
+
+
+ + + +
+
+ ${object.name},

+ Se solicitó un restablecimiento de contraseña para la cuenta de Roomdoo vinculada a este correo electrónico. + Puede cambiar su contraseña siguiendo este enlace que permanecerá válido durante 15 minutos:
+ + + + + + + + + + + Puede ignorar este correo electrónico si no lo esperaba.

+ Gracias, + % 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 +
+
+
+ + +
+ Powered by Odoo +
+
+
+ ${object.lang} + +
+ +
+
diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index e884b0fbe1..5e13a3abb7 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -24,6 +24,7 @@ "data/auth_jwt_validator.xml", "views/pms_property_views.xml", "views/res_users_views.xml", + "views/res_company_views.xml", ], "demo": [ "demo/pms_api_rest_master_data.xml", diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index eda4427130..446b67bc24 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -9,6 +9,8 @@ class PmsApiRestUserInput(Datamodel): 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) class PmsApiRestUserOutput(Datamodel): diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 4713a4d6b7..66a2c10031 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -1,3 +1,4 @@ +from . import res_company from . import pms_property from . import res_users from . import account_payment diff --git a/pms_api_rest/models/res_company.py b/pms_api_rest/models/res_company.py new file mode 100644 index 0000000000..abe5b0df5e --- /dev/null +++ b/pms_api_rest/models/res_company.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + url_app = fields.Char(string="Url App", help="Url to identify the app") diff --git a/pms_api_rest/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index 5979eea09b..55e5fc2848 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -1,9 +1,14 @@ import base64 import tempfile import os +import werkzeug.exceptions + 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 AccessDenied +from datetime import datetime, timedelta + from odoo import _ @@ -113,8 +118,8 @@ def change_password(self, user_id, input_data): if user: try: user.with_user(user)._check_credentials(input_data.password, None) - except: - raise MissingError(_("Wrong password")) + except AccessDenied: + raise werkzeug.exceptions.Unauthorized(_("Wrong password")) user.change_password(input_data.password, input_data.newPassword) @@ -124,3 +129,71 @@ def change_password(self, user_id, input_data): login=user.login, ) + @restapi.method( + [ + ( + [ + "/p/reset-password", + ], + "PATCH", + ) + ], + 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.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.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/views/res_company_views.xml b/pms_api_rest/views/res_company_views.xml new file mode 100644 index 0000000000..c0a8333425 --- /dev/null +++ b/pms_api_rest/views/res_company_views.xml @@ -0,0 +1,12 @@ + + + + res.company + + + + + + + + From b76a4b07bcb41e39db280b3931eb395d454beefe Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 23 May 2023 17:41:54 +0200 Subject: [PATCH 402/547] [IMP]pms-api-rest: url field changed to url param in send reset mail password service --- pms/__manifest__.py | 1 - pms/data/pms_reset_password_email.xml | 109 ---------------------- pms_api_rest/__manifest__.py | 1 - pms_api_rest/datamodels/pms_user.py | 1 + pms_api_rest/models/__init__.py | 1 - pms_api_rest/models/res_company.py | 7 -- pms_api_rest/services/pms_user_service.py | 2 +- pms_api_rest/views/res_company_views.xml | 12 --- 8 files changed, 2 insertions(+), 132 deletions(-) delete mode 100644 pms/data/pms_reset_password_email.xml delete mode 100644 pms_api_rest/models/res_company.py delete mode 100644 pms_api_rest/views/res_company_views.xml diff --git a/pms/__manifest__.py b/pms/__manifest__.py index 8ea4363e46..8149126c27 100644 --- a/pms/__manifest__.py +++ b/pms/__manifest__.py @@ -42,7 +42,6 @@ "data/pms_precheckin_invitation_email_template.xml", "data/pms_data.xml", "data/traveller_report_paperformat.xml", - "data/pms_reset_password_email.xml", "report/pms_folio.xml", "report/pms_folio_templates.xml", "report/traveller_report_action.xml", diff --git a/pms/data/pms_reset_password_email.xml b/pms/data/pms_reset_password_email.xml deleted file mode 100644 index d54926c564..0000000000 --- a/pms/data/pms_reset_password_email.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - Pms Reset Password - - Restablecer Contraseña - "${object.company_id.name | safe}" <${(object.company_id.email or user.email) | safe}> - ${object.email_formatted | safe} - - - - -
- - - - - - - - - - - - - - - -
- - - -
- Tu Cuenta en Roomdoo
- - ${object.name} - -
- ${object.company_id.name} -
-
-
-
- - - -
-
- ${object.name},

- Se solicitó un restablecimiento de contraseña para la cuenta de Roomdoo vinculada a este correo electrónico. - Puede cambiar su contraseña siguiendo este enlace que permanecerá válido durante 15 minutos:
- - - - - - - - - - - Puede ignorar este correo electrónico si no lo esperaba.

- Gracias, - % 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 -
-
-
- - -
- Powered by Odoo -
-
-
- ${object.lang} - -
- -
-
diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 5e13a3abb7..e884b0fbe1 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -24,7 +24,6 @@ "data/auth_jwt_validator.xml", "views/pms_property_views.xml", "views/res_users_views.xml", - "views/res_company_views.xml", ], "demo": [ "demo/pms_api_rest_master_data.xml", diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index 446b67bc24..611c715303 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -11,6 +11,7 @@ class PmsApiRestUserInput(Datamodel): 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): diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 66a2c10031..4713a4d6b7 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -1,4 +1,3 @@ -from . import res_company from . import pms_property from . import res_users from . import account_payment diff --git a/pms_api_rest/models/res_company.py b/pms_api_rest/models/res_company.py deleted file mode 100644 index abe5b0df5e..0000000000 --- a/pms_api_rest/models/res_company.py +++ /dev/null @@ -1,7 +0,0 @@ -from odoo import fields, models - - -class ResCompany(models.Model): - _inherit = "res.company" - - url_app = fields.Char(string="Url App", help="Url to identify the app") diff --git a/pms_api_rest/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index 55e5fc2848..3640e04845 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -172,7 +172,7 @@ def send_mail_to_reset_password(self, input_data): return False expiration_datetime = datetime.now() + timedelta(minutes=15) user.partner_id.sudo().signup_prepare(expiration=expiration_datetime) - template.send_mail(user.id, force_send=True) + template.with_context({'app_url': input_data.url}).send_mail(user.id, force_send=True) return True return False diff --git a/pms_api_rest/views/res_company_views.xml b/pms_api_rest/views/res_company_views.xml deleted file mode 100644 index c0a8333425..0000000000 --- a/pms_api_rest/views/res_company_views.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - res.company - - - - - - - - From dbd13d8ecd2d7001a32154c2e6a33da3a82789f1 Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 29 May 2023 14:12:43 +0200 Subject: [PATCH 403/547] [IMP]pms_api_rest: added mail template to reset paswword --- pms_api_rest/__manifest__.py | 1 + .../data/pms_app_reset_password_template.xml | 99 +++++++++++++++++++ pms_api_rest/services/pms_user_service.py | 2 +- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 pms_api_rest/data/pms_app_reset_password_template.xml diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index e884b0fbe1..9120e04e56 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -22,6 +22,7 @@ "data": [ "data/sql_reports.xml", "data/auth_jwt_validator.xml", + "data/pms_app_reset_password_template.xml", "views/pms_property_views.xml", "views/res_users_views.xml", ], 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..6de5976775 --- /dev/null +++ b/pms_api_rest/data/pms_app_reset_password_template.xml @@ -0,0 +1,99 @@ + + + + + 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/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index 3640e04845..23cdcf5bfa 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -166,7 +166,7 @@ def reset_password(self, input_data): 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.pms_reset_password_email").id + 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 From bc8761642761a940c46f652d072b46a5a6e7d08f Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 29 May 2023 17:07:13 +0200 Subject: [PATCH 404/547] [IMP] pms_api_rest: amenities in room name --- pms_api_rest/datamodels/pms_amenity.py | 1 + pms_api_rest/services/pms_amenity_service.py | 1 + pms_api_rest/services/pms_room_service.py | 8 ++++---- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/datamodels/pms_amenity.py b/pms_api_rest/datamodels/pms_amenity.py index f52cfceba5..b36ccdae9c 100644 --- a/pms_api_rest/datamodels/pms_amenity.py +++ b/pms_api_rest/datamodels/pms_amenity.py @@ -15,3 +15,4 @@ class PmsAmenityInfo(Datamodel): 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/services/pms_amenity_service.py b/pms_api_rest/services/pms_amenity_service.py index 486c249d77..ac6e4a3fdf 100644 --- a/pms_api_rest/services/pms_amenity_service.py +++ b/pms_api_rest/services/pms_amenity_service.py @@ -49,6 +49,7 @@ def get_amenities(self, amenities_search_param): id=amenity.id, name=amenity.name, amenityTypeId=amenity.pms_amenity_type_id.id, + addInRoomName=amenity.is_add_code_room_name, ) ) return result_amenities diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 702c753666..a96855b049 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -80,10 +80,10 @@ def get_rooms(self, room_search_param): # 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 + # 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, From f4f6190fcec531812249e6bd06a10694041d3d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 31 May 2023 14:38:57 +0200 Subject: [PATCH 405/547] [FIX] Import odoo.odoo pms_user_service --- pms_api_rest/services/pms_user_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index 23cdcf5bfa..cf2845429b 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -12,7 +12,7 @@ from odoo import _ -from odoo.odoo.exceptions import MissingError +from odoo.exceptions import MissingError class PmsRoomTypeClassService(Component): From 2e66c0028b944a992a89ecca12584ea5b8ffe516 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 31 May 2023 17:51:57 +0200 Subject: [PATCH 406/547] [FIX] pms-api-rest: fix login service else none when no email --- pms_api_rest/services/pms_login_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 5e0a04a810..92c601cac2 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -73,7 +73,7 @@ def login(self, user): expirationDate=timestamp_expire_in_a_min, userId=user_record.id, userName=user_record.name, - userEmail=user_record.email, + userEmail=user_record.email if user_record.email else None, userPhone=user_record.phone if user_record.phone else None, defaultPropertyId=user_record.pms_property_id.id, defaultPropertyName=user_record.pms_property_id.name, From 72474cce45708d404eed6842ac6d2567e5ec33c7 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 1 Jun 2023 11:57:54 +0200 Subject: [PATCH 407/547] [IMP] pms_api_rest: add sale channel icon in datamodel and service --- pms_api_rest/datamodels/pms_sale_channel.py | 1 + pms_api_rest/services/pms_sale_channel_service.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_sale_channel.py b/pms_api_rest/datamodels/pms_sale_channel.py index 8a734b422a..57d1ecd412 100644 --- a/pms_api_rest/datamodels/pms_sale_channel.py +++ b/pms_api_rest/datamodels/pms_sale_channel.py @@ -13,3 +13,4 @@ class PmsSaleChannelInfo(Datamodel): id = fields.Integer(required=True, allow_none=False) name = fields.String(required=True, allow_none=False) channelType = fields.String(required=True, allow_none=True) + icon = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_sale_channel_service.py b/pms_api_rest/services/pms_sale_channel_service.py index 5d6dd7a887..7c209912eb 100644 --- a/pms_api_rest/services/pms_sale_channel_service.py +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -62,6 +62,9 @@ def get_sale_channels(self, sale_channel_search_param): channelType=sale_channel.channel_type if sale_channel.channel_type else None, + icon=sale_channel.icon + if sale_channel.icon + else None, ) ) return result_sale_channels From db7f6d10266afe8ce18134c3f2734ab29ea9e8f9 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 7 Jun 2023 11:19:42 +0200 Subject: [PATCH 408/547] [IMP] pms_api_rest: add filter in get_folios --- pms_api_rest/services/pms_folio_service.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 7cf8013fb0..4ab2e00d96 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -51,9 +51,7 @@ def get_folio(self, folio_id): 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=dict(folio.fields_get(["state"])["state"]["selection"])[ - folio.state - ], + state=folio.state, amountTotal=round(folio.amount_total, 2), reservationType=folio.reservation_type, pendingAmount=folio.pending_amount, @@ -164,6 +162,7 @@ def get_folios(self, folio_search_param): 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)) @@ -242,9 +241,7 @@ def get_folios(self, folio_search_param): PmsFolioShortInfo( id=folio.id, name=folio.name, - state=dict(folio.fields_get(["state"])["state"]["selection"])[ - folio.state - ], + 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, From 44200515b1d63053ceb63ffc81118c09aaac9a6c Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 19 Jun 2023 11:31:08 +0200 Subject: [PATCH 409/547] [IMP][DEMO] pms_api_rest: add fields in reservation datamodel --- pms_api_rest/datamodels/pms_reservation.py | 3 +++ pms_api_rest/services/pms_reservation_service.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 2da39e73b9..ae116abc8c 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -73,8 +73,11 @@ class PmsReservationInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 63ea985a8b..575619ab7d 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -108,10 +108,15 @@ def get_reservation(self, reservation_id, pms_search_param): 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 From e19d62f8b77eb9756f98b7c8b11c9d191b025ede Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 19 Jun 2023 11:36:05 +0200 Subject: [PATCH 410/547] [IMP][DEMO] pms_api_rest: add fields in reservation datamodel and rmv filtered in get_checkin_partners --- pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/services/pms_reservation_service.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index ae116abc8c..98a14b71e2 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -62,6 +62,7 @@ class PmsReservationInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 575619ab7d..cd7468545a 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -98,6 +98,7 @@ def get_reservation(self, reservation_id, pms_search_param): )[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, @@ -537,9 +538,10 @@ def get_checkin_partners(self, reservation_id): pass else: # TODO Review state draft - for checkin_partner in reservation.checkin_partner_ids.filtered( - lambda ch: ch.state != "dummy" - ): + #.filtered( + # lambda ch: ch.state != "dummy" + # ) + for checkin_partner in reservation.checkin_partner_ids: if checkin_partner.document_expedition_date: document_expedition_date = ( checkin_partner.document_expedition_date.strftime("%d/%m/%Y") @@ -599,6 +601,7 @@ def get_checkin_partners(self, reservation_id): checkinPartnerState=checkin_partner.state, ) ) + print(checkin_partners) return checkin_partners @restapi.method( From 916b41b52e8268202481996ddd66b65ea494aff2 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 21 Jun 2023 11:20:20 +0200 Subject: [PATCH 411/547] [REF]pms_api_rest: changed search domain operator like to ilike --- pms_api_rest/services/pms_product_service.py | 2 +- pms_api_rest/services/pms_reservation_service.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_product_service.py b/pms_api_rest/services/pms_product_service.py index c550c799ca..7395165ecd 100644 --- a/pms_api_rest/services/pms_product_service.py +++ b/pms_api_rest/services/pms_product_service.py @@ -28,7 +28,7 @@ class PmsProductService(Component): def get_products(self, product_search_param): domain = [("sale_ok", "=", True), ("is_pms_available", "=", True)] if product_search_param.name: - domain.append(("name", "like", product_search_param.name)) + domain.append(("name", "ilike", product_search_param.name)) if product_search_param.pmsPropertyId: domain.extend( [ diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index cd7468545a..12c12812d4 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -601,7 +601,6 @@ def get_checkin_partners(self, reservation_id): checkinPartnerState=checkin_partner.state, ) ) - print(checkin_partners) return checkin_partners @restapi.method( From 947924e18157aaf7c0909bea420d0aed4b5f5a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 11 Jun 2023 09:58:34 +0200 Subject: [PATCH 412/547] [IMP]pms_api_rest: service calendar performance improvement --- pms_api_rest/datamodels/pms_calendar.py | 7 + pms_api_rest/services/pms_calendar_service.py | 296 +++++++++++++----- 2 files changed, 218 insertions(+), 85 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 6b9e76c8b5..c7c515abdf 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -72,6 +72,13 @@ class PmsCalendarInfo(Datamodel): isReselling = fields.Boolean(required=False, allow_none=False) +class PmsCalendarRenderInfo(Datamodel): + _name = "pms.calendar.render.info" + roomId = fields.Integer(required=True, allow_none=False) + roomTypeId = fields.String(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) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 7686e02aae..2484a179a9 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -21,32 +21,72 @@ class PmsCalendarService(Component): ) ], input_param=Datamodel("pms.calendar.search.param"), - output_param=Datamodel("pms.calendar.info", is_list=True), + output_param=Datamodel("pms.calendar.render.info", is_list=True), auth="jwt_api_pms", ) def get_calendar(self, calendar_search_param): + """ + Optimized query to get calendar, with the next schema: + [ + { + "roomId":INT, + "roomTypeId":INT, + "dates":[ + { + "date":"2023-06-25T00:00:00", + "reservationLines":[ + { + "folioId":INT, + "id":INT, + "reservationName":"203/23/000105/1", + "isFirstNight":false, + "pendingPayment":0, + "partnerId":null, + "numNotifications":0, + "priceDayTotalServices":0, + "partnerName":null, + "isLastNight":false, + "splitted":false, + "date":"2023-06-25T00:00:00", + "adults":0, + "nextLineSplitted":false, + "toAssign":false, + "totalPrice":0, + "state":"arrival_delayed", + "previous_itemSplitted":false, + "reservationId":466936, + "priceDayTotal":0, + "roomTypeName":"EST", + "roomId":1913, + "closureReasonId":1, + "reservationType":"out" + }, + ... + ] + }, + ... + ] + }, + ... + ] + """ 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 - # subselect_sum_services_price = ( - # "(" - # " SELECT COALESCE(SUM(s.price_day_total),0) price_day_total_services " - # " FROM pms_service_line s " - # " WHERE s.reservation_id = night.reservation_id " - # " AND s.date = night.date AND NOT s.is_board_service " - # " ) " - # ) + # group by room_id, date and take account the first line for + # reservation to build de reservationLines + # array only in the first line 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", + "id": "night.id as id", + "state": "night.state as state", + "date": "DATE(night.date) as date", + "room_id": "night.room_id as room_id", + "room_type_name": "pms_room_type.default_code as room_type_name", + "to_assign": "reservation.to_assign as to_assign", + "splitted": "reservation.splitted as splitted", + "partner_id": "reservation.partner_id as partner_id", "partner_name": "reservation.partner_name", "folio_id": "folio.id", "reservation_id": "reservation.id", @@ -63,7 +103,6 @@ def get_calendar(self, calendar_search_param): # "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""" @@ -79,84 +118,171 @@ def get_calendar(self, calendar_search_param): AND (night.date in %s) AND (night.state != 'cancel') AND (night.occupies_availability = True) + ORDER BY night.room_id, night.date """, ( 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} + result = self.env.cr.dictfetchall() + response = [] + CalendarRenderInfo = self.env.datamodels["pms.calendar.render.info"] + last_date = date_from - timedelta(days=1) + for index, item in enumerate(result): + last_reservation_id = ( + result[index - 1]["reservation_id"] if index > 0 else False ) - - 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, + last_room_id = result[index - 1]["room_id"] if index > 0 else False + # If the room_id is different from the previous one, we create a new + # room object + if item["room_id"] != last_room_id: + response.append( + CalendarRenderInfo( + roomId=item["room_id"], + roomTypeId=item["room_type_name"], + dates=[], + ) ) - 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, + # We use index_date to know the index of the + # last date added with reservationLines + # the index is avoid to use because we need + # to add dates without reservationLines + index_date = 0 + # If the date is the next one, and is the same reservation, we add + # the reservation line to the last date and add avoid date in main array + if ( + item["date"] == last_date + timedelta(days=1) + and item["reservation_id"] == last_reservation_id + ): + response[-1].dates[index_date]["reservationLines"].append( + self._build_reservation_line(item) + ) + response[-1].dates.append( + { + "date": datetime.combine( + item["date"], datetime.min.time() + ).isoformat(), + "reservationLines": [], + } + ) + last_date = item["date"] + # If the date not is the next one, we create a new date object + # withouth reservation lines + elif item["date"] != last_date + timedelta(days=1): + response[-1].dates.extend( + self._build_dates_without_reservation_lines( + date_from=last_date + timedelta(days=1), + date_to=item["date"] - timedelta(days=1), + ) ) - 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), - # TODO: priceDayTotalServices=round(line["price_day_total_services"], 0), - priceDayTotalServices=0, - # TODO: line.reservation_id.message_needaction_counter is computed field, - numNotifications=0, - adults=line["adults"], - nextLineSplitted=next_line_splitted, - previousLineSplitted=previous_line_splitted, - closureReasonId=line["closure_reason_id"], - isReselling=line["is_reselling"], + response[-1].dates.append( + { + "date": datetime.combine( + item["date"], datetime.min.time() + ).isoformat(), + "reservationLines": [ + self._build_reservation_line( + item=item, + next_item=False + if not item["splitted"] + else result[index + 1], + previous_item=False + if not item["splitted"] + else result[index - 1], + ) + ], + } + ) + last_date = item["date"] + index_date = len(response[-1].dates) - 1 + # else, the date is the next one, but the reservation is different + # so we create a new date object with the reservation line + else: + response[-1].dates.append( + { + "date": datetime.combine( + item["date"], datetime.min.time() + ).isoformat(), + "reservationLines": [ + self._build_reservation_line( + item=item, + next_item=False + if (not item["splitted"] or item["date"] == date_to) + else result[index + 1], + previous_item=False + if (not item["splitted"] or item["date"] == date_from) + else result[index - 1], + ) + ], + } ) + last_date = item["date"] + index_date = len(response[-1].dates) - 1 + return response + + def _build_dates_without_reservation_lines(self, date_from, date_to): + count_nights = (date_to - date_from).days + 1 + target_dates = [date_from + timedelta(days=x) for x in range(count_nights)] + return [ + { + "date": datetime.combine(date, datetime.min.time()).isoformat(), + "reservationLines": [], + } + for date in target_dates + ] + + def _build_reservation_line(self, item, next_item=False, previous_item=False): + # next_item is sent if the current item is splitted + # and the date not is the last in the range + # (because in the last date, the reservation line no is + # show with the next date splitted) + # the same for previous_item + next_itemSplitted = ( + item["splitted"] + and next_item + and item["date"] < item["checkout"] - timedelta(days=1) + and ( + next_item["room_id"] != item["room_id"] + or next_item["reservation_id"] != item["reservation_id"] ) - return result_lines + ) + previous_itemSplitted = ( + item["splitted"] + and previous_item + and item["date"] > item["checkin"] + timedelta(days=1) + and ( + previous_item["room_id"] != item["room_id"] + or previous_item["reservation_id"] != item["reservation_id"] + ) + ) + return { + "id": item["id"], + "state": item["state"], + "date": datetime.combine(item["date"], datetime.min.time()).isoformat(), + "roomId": item["room_id"], + "roomTypeName": item["room_type_name"], + "toAssign": item["to_assign"], + "splitted": item["splitted"], + "partnerId": item["partner_id"], + "partnerName": item["partner_name"], + "folioId": item["folio_id"], + "reservationId": item["reservation_id"], + "reservationName": item["reservation_name"], + "reservationType": item["reservation_type"], + "adults": item["adults"], + "priceDayTotal": item["price_day_total"], + "closureReasonId": item["closure_reason_id"], + "isFirstNight": item["date"] == item["checkin"], + "isLastNight": item["date"] == item["checkout"] - timedelta(days=1), + "totalPrice": item["price_total"], + "pendingPayment": item["folio_pending_amount"], + "numNotifications": 0, + "nextLineSplitted": next_itemSplitted, + "previous_itemSplitted": previous_itemSplitted, + "priceDayTotalServices": 0, + "isReselling": item["is_reselling"], + } @restapi.method( [ From 7875cc5f46ecf95b4ff3a36f1a25d4cf1130b1c5 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 15 Jun 2023 10:36:42 +0200 Subject: [PATCH 413/547] [IMP] pms-api-rest: improvement performance planning --- pms_api_rest/datamodels/pms_calendar.py | 2 +- pms_api_rest/services/pms_calendar_service.py | 174 +++++++++++++++++- 2 files changed, 174 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index c7c515abdf..6361f4ec0b 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -75,7 +75,7 @@ class PmsCalendarInfo(Datamodel): class PmsCalendarRenderInfo(Datamodel): _name = "pms.calendar.render.info" roomId = fields.Integer(required=True, allow_none=False) - roomTypeId = fields.String(required=True, allow_none=False) + roomTypeId = fields.Integer(required=True, allow_none=False) dates = fields.List(fields.Dict(required=True, allow_none=False)) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 2484a179a9..87ca1ca780 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -15,7 +15,7 @@ class PmsCalendarService(Component): [ ( [ - "/", + "/old-calendar", ], "GET", ) @@ -129,6 +129,7 @@ def get_calendar(self, calendar_search_param): response = [] CalendarRenderInfo = self.env.datamodels["pms.calendar.render.info"] last_date = date_from - timedelta(days=1) + for index, item in enumerate(result): last_reservation_id = ( result[index - 1]["reservation_id"] if index > 0 else False @@ -284,6 +285,177 @@ def _build_reservation_line(self, item, next_item=False, previous_item=False): "isReselling": item["is_reselling"], } + @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_calendar_new(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": "date_room.date date", + "room_id": "date_room.room_id room_id", + "room_type_id": "date_room.room_type_id room_type_id", + "id": "line.id id", + "state": "line.state state", + "price_day_total": "line.price_day_total price_day_total", + "to_assign": "reservation.to_assign to_assign", + "splitted": "reservation.splitted splitted", + "partner_id": "reservation.partner_id partner_id", + "partner_name": "reservation.partner_name partner_name", + "folio_id": "reservation.folio_id folio_id", + "reservation_id": "reservation.id reservation_id", + "reservation_name": "reservation.name reservation_name", + "reservation_type": "reservation.reservation_type reservation_type", + "checkin": "reservation.checkin checkin", + "checkout": "reservation.checkout checkout", + "price_total": "reservation.price_total price_total", + "adults": "reservation.adults adults", + "folio_pending_amount": "folio.pending_amount folio_pending_amount", + "closure_reason_id": "folio.closure_reason_id closure_reason_id", + } + 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, + rooms.id room_id, + rooms.room_type_id room_type_id + FROM (SELECT (CURRENT_DATE + date ) date + FROM generate_series(date %s- CURRENT_DATE, date %s - CURRENT_DATE) date + ) dates, + (SELECT id, room_type_id FROM pms_room rooms WHERE pms_property_id = %s) rooms + ) date_room + LEFT OUTER JOIN ( SELECT id, state, price_day_total, room_id, date, reservation_id + FROM pms_reservation_line + WHERE pms_property_id = %s AND state != 'cancel' AND occupies_availability = true + ) line ON line.room_id = date_room.room_id AND line.date = date_room.date + LEFT OUTER JOIN pms_reservation reservation ON line.reservation_id = reservation.id + LEFT OUTER JOIN pms_folio folio ON reservation.folio_id = folio.id + ORDER BY date_room.room_id, date_room.date + """, + ( + calendar_search_param.dateFrom, + calendar_search_param.dateTo, + calendar_search_param.pmsPropertyId, + 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): + if last_room_id != item['room_id']: + last_room_id = item['room_id'] + response.append( + CalendarRenderInfo( + roomId=item["room_id"], + roomTypeId=item["room_type_id"], + dates=[ + { + "date": datetime.combine(item['date'], datetime.min.time()).isoformat(), + "reservationLines": [], + } + ], + ) + ) + else: + response[-1].dates.append( + { + "date": datetime.combine(item['date'], datetime.min.time()).isoformat(), + "reservationLines": [], + } + ) + if item['reservation_id'] is not None and item['reservation_id'] != last_reservation_id: + response[-1].dates[-1]['reservationLines'].append( + self.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( + self.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'] + return response + + def build_reservation_line_info(self, 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'], + "folioPendingAmount": 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, + } + @restapi.method( [ ( From 3c9c93888b34e38f1643e12f7a3494a94d26ca10 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 19 Jun 2023 10:04:46 +0200 Subject: [PATCH 414/547] [IMP] pms-api-rest: old & new planning service --- pms_api_rest/services/pms_calendar_service.py | 325 ++++++------------ 1 file changed, 97 insertions(+), 228 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 87ca1ca780..c1773e64c2 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -21,72 +21,25 @@ class PmsCalendarService(Component): ) ], input_param=Datamodel("pms.calendar.search.param"), - output_param=Datamodel("pms.calendar.render.info", is_list=True), + output_param=Datamodel("pms.calendar.info", is_list=True), auth="jwt_api_pms", ) - def get_calendar(self, calendar_search_param): - """ - Optimized query to get calendar, with the next schema: - [ - { - "roomId":INT, - "roomTypeId":INT, - "dates":[ - { - "date":"2023-06-25T00:00:00", - "reservationLines":[ - { - "folioId":INT, - "id":INT, - "reservationName":"203/23/000105/1", - "isFirstNight":false, - "pendingPayment":0, - "partnerId":null, - "numNotifications":0, - "priceDayTotalServices":0, - "partnerName":null, - "isLastNight":false, - "splitted":false, - "date":"2023-06-25T00:00:00", - "adults":0, - "nextLineSplitted":false, - "toAssign":false, - "totalPrice":0, - "state":"arrival_delayed", - "previous_itemSplitted":false, - "reservationId":466936, - "priceDayTotal":0, - "roomTypeName":"EST", - "roomId":1913, - "closureReasonId":1, - "reservationType":"out" - }, - ... - ] - }, - ... - ] - }, - ... - ] - """ + def get_old_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 - # group by room_id, date and take account the first line for - # reservation to build de reservationLines - # array only in the first line + selected_fields_mapper = { - "id": "night.id as id", - "state": "night.state as state", - "date": "DATE(night.date) as date", - "room_id": "night.room_id as room_id", - "room_type_name": "pms_room_type.default_code as room_type_name", - "to_assign": "reservation.to_assign as to_assign", - "splitted": "reservation.splitted as splitted", - "partner_id": "reservation.partner_id as partner_id", + "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", @@ -103,193 +56,104 @@ def get_calendar(self, calendar_search_param): # "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) - ORDER BY night.room_id, night.date - """, + {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 = self.env.cr.dictfetchall() - response = [] - CalendarRenderInfo = self.env.datamodels["pms.calendar.render.info"] - last_date = date_from - timedelta(days=1) - - for index, item in enumerate(result): - last_reservation_id = ( - result[index - 1]["reservation_id"] if index > 0 else False + 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} ) - last_room_id = result[index - 1]["room_id"] if index > 0 else False - # If the room_id is different from the previous one, we create a new - # room object - if item["room_id"] != last_room_id: - response.append( - CalendarRenderInfo( - roomId=item["room_id"], - roomTypeId=item["room_type_name"], - dates=[], - ) - ) - # We use index_date to know the index of the - # last date added with reservationLines - # the index is avoid to use because we need - # to add dates without reservationLines - index_date = 0 - # If the date is the next one, and is the same reservation, we add - # the reservation line to the last date and add avoid date in main array - if ( - item["date"] == last_date + timedelta(days=1) - and item["reservation_id"] == last_reservation_id - ): - response[-1].dates[index_date]["reservationLines"].append( - self._build_reservation_line(item) - ) - response[-1].dates.append( - { - "date": datetime.combine( - item["date"], datetime.min.time() - ).isoformat(), - "reservationLines": [], - } - ) - last_date = item["date"] - # If the date not is the next one, we create a new date object - # withouth reservation lines - elif item["date"] != last_date + timedelta(days=1): - response[-1].dates.extend( - self._build_dates_without_reservation_lines( - date_from=last_date + timedelta(days=1), - date_to=item["date"] - timedelta(days=1), - ) + + 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, ) - response[-1].dates.append( - { - "date": datetime.combine( - item["date"], datetime.min.time() - ).isoformat(), - "reservationLines": [ - self._build_reservation_line( - item=item, - next_item=False - if not item["splitted"] - else result[index + 1], - previous_item=False - if not item["splitted"] - else result[index - 1], - ) - ], - } + 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, ) - last_date = item["date"] - index_date = len(response[-1].dates) - 1 - # else, the date is the next one, but the reservation is different - # so we create a new date object with the reservation line - else: - response[-1].dates.append( - { - "date": datetime.combine( - item["date"], datetime.min.time() - ).isoformat(), - "reservationLines": [ - self._build_reservation_line( - item=item, - next_item=False - if (not item["splitted"] or item["date"] == date_to) - else result[index + 1], - previous_item=False - if (not item["splitted"] or item["date"] == date_from) - else result[index - 1], - ) - ], - } + 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"], ) - last_date = item["date"] - index_date = len(response[-1].dates) - 1 - return response - - def _build_dates_without_reservation_lines(self, date_from, date_to): - count_nights = (date_to - date_from).days + 1 - target_dates = [date_from + timedelta(days=x) for x in range(count_nights)] - return [ - { - "date": datetime.combine(date, datetime.min.time()).isoformat(), - "reservationLines": [], - } - for date in target_dates - ] - - def _build_reservation_line(self, item, next_item=False, previous_item=False): - # next_item is sent if the current item is splitted - # and the date not is the last in the range - # (because in the last date, the reservation line no is - # show with the next date splitted) - # the same for previous_item - next_itemSplitted = ( - item["splitted"] - and next_item - and item["date"] < item["checkout"] - timedelta(days=1) - and ( - next_item["room_id"] != item["room_id"] - or next_item["reservation_id"] != item["reservation_id"] - ) - ) - previous_itemSplitted = ( - item["splitted"] - and previous_item - and item["date"] > item["checkin"] + timedelta(days=1) - and ( - previous_item["room_id"] != item["room_id"] - or previous_item["reservation_id"] != item["reservation_id"] ) - ) - return { - "id": item["id"], - "state": item["state"], - "date": datetime.combine(item["date"], datetime.min.time()).isoformat(), - "roomId": item["room_id"], - "roomTypeName": item["room_type_name"], - "toAssign": item["to_assign"], - "splitted": item["splitted"], - "partnerId": item["partner_id"], - "partnerName": item["partner_name"], - "folioId": item["folio_id"], - "reservationId": item["reservation_id"], - "reservationName": item["reservation_name"], - "reservationType": item["reservation_type"], - "adults": item["adults"], - "priceDayTotal": item["price_day_total"], - "closureReasonId": item["closure_reason_id"], - "isFirstNight": item["date"] == item["checkin"], - "isLastNight": item["date"] == item["checkout"] - timedelta(days=1), - "totalPrice": item["price_total"], - "pendingPayment": item["folio_pending_amount"], - "numNotifications": 0, - "nextLineSplitted": next_itemSplitted, - "previous_itemSplitted": previous_itemSplitted, - "priceDayTotalServices": 0, - "isReselling": item["is_reselling"], - } + return result_lines @restapi.method( [ ( [ - "/", + "/temp-calendar", ], "GET", ) @@ -340,7 +204,8 @@ def get_calendar_new(self, calendar_search_param): ) date_room LEFT OUTER JOIN ( SELECT id, state, price_day_total, room_id, date, reservation_id FROM pms_reservation_line - WHERE pms_property_id = %s AND state != 'cancel' AND occupies_availability = true + WHERE pms_property_id = %s AND state != 'cancel' + AND occupies_availability = true AND date < %s ) line ON line.room_id = date_room.room_id AND line.date = date_room.date LEFT OUTER JOIN pms_reservation reservation ON line.reservation_id = reservation.id LEFT OUTER JOIN pms_folio folio ON reservation.folio_id = folio.id @@ -351,6 +216,7 @@ def get_calendar_new(self, calendar_search_param): calendar_search_param.dateTo, calendar_search_param.pmsPropertyId, calendar_search_param.pmsPropertyId, + calendar_search_param.dateTo, ), ) result = self.env.cr.dictfetchall() @@ -361,6 +227,7 @@ def get_calendar_new(self, calendar_search_param): for index, item in enumerate(result): if last_room_id != item['room_id']: last_room_id = item['room_id'] + last_reservation_id = False response.append( CalendarRenderInfo( roomId=item["room_id"], @@ -407,6 +274,8 @@ def get_calendar_new(self, calendar_search_param): ) ) last_reservation_id = item['reservation_id'] + else: + last_reservation_id = False return response def build_reservation_line_info(self, calendar_item, previous_item=False, next_item=False): From 729edc2d945041963da0624b69af78804b943438 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 19 Jun 2023 12:37:13 +0200 Subject: [PATCH 415/547] [imp] pms-api-rest: add fields capacity & room_type_class_id to planning service --- pms_api_rest/datamodels/pms_calendar.py | 2 + pms_api_rest/services/pms_calendar_service.py | 66 +++++++++++-------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 6361f4ec0b..8208b9d364 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -75,6 +75,8 @@ class PmsCalendarInfo(Datamodel): 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)) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index c1773e64c2..ac8c1e5591 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -167,26 +167,28 @@ def get_calendar_new(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() selected_fields_mapper = { - "date": "date_room.date date", - "room_id": "date_room.room_id room_id", - "room_type_id": "date_room.room_type_id room_type_id", - "id": "line.id id", - "state": "line.state state", - "price_day_total": "line.price_day_total price_day_total", - "to_assign": "reservation.to_assign to_assign", - "splitted": "reservation.splitted splitted", - "partner_id": "reservation.partner_id partner_id", - "partner_name": "reservation.partner_name partner_name", - "folio_id": "reservation.folio_id folio_id", - "reservation_id": "reservation.id reservation_id", - "reservation_name": "reservation.name reservation_name", - "reservation_type": "reservation.reservation_type reservation_type", - "checkin": "reservation.checkin checkin", - "checkout": "reservation.checkout checkout", - "price_total": "reservation.price_total price_total", - "adults": "reservation.adults adults", - "folio_pending_amount": "folio.pending_amount folio_pending_amount", - "closure_reason_id": "folio.closure_reason_id closure_reason_id", + "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", + "folio_pending_amount": "f.pending_amount folio_pending_amount", + "closure_reason_id": "f.closure_reason_id closure_reason_id", } selected_fields_sql = list(selected_fields_mapper.values()) sql_select = "SELECT %s" % ", ".join(selected_fields_sql) @@ -195,21 +197,27 @@ def get_calendar_new(self, calendar_search_param): {sql_select} FROM (SELECT dates.date, - rooms.id room_id, - rooms.room_type_id room_type_id + r_rt_rtc.room_id, + r_rt_rtc.capacity, + r_rt_rtc.room_type_id, + r_rt_rtc.room_type_class_id FROM (SELECT (CURRENT_DATE + date ) date FROM generate_series(date %s- CURRENT_DATE, date %s - CURRENT_DATE) date ) dates, - (SELECT id, room_type_id FROM pms_room rooms WHERE pms_property_id = %s) rooms - ) date_room + (SELECT r.id room_id, r.capacity, rt.id room_type_id, rtc.id room_type_class_id + 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 pms_property_id = %s) r_rt_rtc + ) dr LEFT OUTER JOIN ( SELECT id, state, price_day_total, room_id, date, reservation_id FROM pms_reservation_line WHERE pms_property_id = %s AND state != 'cancel' AND occupies_availability = true AND date < %s - ) line ON line.room_id = date_room.room_id AND line.date = date_room.date - LEFT OUTER JOIN pms_reservation reservation ON line.reservation_id = reservation.id - LEFT OUTER JOIN pms_folio folio ON reservation.folio_id = folio.id - ORDER BY date_room.room_id, date_room.date + ) l ON l.room_id = dr.room_id AND l.date = dr.date + 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.room_id, dr.date """, ( calendar_search_param.dateFrom, @@ -231,6 +239,8 @@ def get_calendar_new(self, calendar_search_param): response.append( CalendarRenderInfo( roomId=item["room_id"], + capacity=item["capacity"], + roomTypeClassId=item["room_type_class_id"], roomTypeId=item["room_type_id"], dates=[ { From 628ebac3dbee464f4273a3200baf07898a66f62d Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 20 Jun 2023 12:37:58 +0200 Subject: [PATCH 416/547] [FIX] pms-api-rest: fix field folio pending amount @ planning service --- pms_api_rest/services/pms_calendar_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index ac8c1e5591..2fafab5ec6 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -326,7 +326,7 @@ def build_reservation_line_info(self, calendar_item, previous_item=False, next_i "checkout": datetime.combine(calendar_item['checkout'], datetime.min.time()).isoformat(), "priceTotal": calendar_item['price_total'], "adults": calendar_item['adults'], - "folioPendingAmount": calendar_item['folio_pending_amount'], + "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) From eeb7a9c6a5c532934fcd7b941336bb746da656f6 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 26 Jun 2023 19:01:29 +0200 Subject: [PATCH 417/547] [IMP] pms-api-rest: add planning calendar headers service --- pms_api_rest/datamodels/pms_calendar.py | 16 ++ pms_api_rest/services/pms_calendar_service.py | 190 +++++++++++++----- 2 files changed, 159 insertions(+), 47 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index 8208b9d364..d40ba56b0a 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -30,6 +30,14 @@ class PmsCalendarSearchParam(Datamodel): pricelistId = 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) @@ -43,6 +51,14 @@ class PmsCalendarDailyInvoicing(Datamodel): 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) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 2fafab5ec6..ce6eb5db5f 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -5,6 +5,54 @@ 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'], + "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, + } + + class PmsCalendarService(Component): _inherit = "base.rest.service" _name = "pms.private.service" @@ -153,7 +201,7 @@ def get_old_calendar(self, calendar_search_param): [ ( [ - "/temp-calendar", + "/", ], "GET", ) @@ -213,7 +261,7 @@ def get_calendar_new(self, calendar_search_param): LEFT OUTER JOIN ( SELECT id, state, price_day_total, room_id, date, reservation_id FROM pms_reservation_line WHERE pms_property_id = %s AND state != 'cancel' - AND occupies_availability = true AND date < %s + AND occupies_availability = true AND date <= %s ) l ON l.room_id = dr.room_id AND l.date = dr.date LEFT OUTER JOIN pms_reservation r ON l.reservation_id = r.id LEFT OUTER JOIN pms_folio f ON r.folio_id = f.id @@ -259,7 +307,7 @@ def get_calendar_new(self, calendar_search_param): ) if item['reservation_id'] is not None and item['reservation_id'] != last_reservation_id: response[-1].dates[-1]['reservationLines'].append( - self.build_reservation_line_info( + build_reservation_line_info( item, previous_item=False if (not item["splitted"] or item["date"] == date_from) @@ -273,7 +321,7 @@ def get_calendar_new(self, calendar_search_param): 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( - self.build_reservation_line_info( + build_reservation_line_info( item, previous_item=False if (not item["splitted"] or item["date"] == date_from) @@ -288,52 +336,100 @@ def get_calendar_new(self, calendar_search_param): last_reservation_id = False return response - def build_reservation_line_info(self, 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"] + + @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( + f""" + SELECT d.date, + bool_or(l.overbooking) overbooking, + SUM(l.price_day_total) daily_billing, + tr.num_total_rooms + - + ( + SELECT COUNT(1) + FROM pms_reservation_line + WHERE date = d.date + AND pms_property_id = %s + AND state != 'cancel' + AND occupies_availability = true + AND room_id IN %s + ) 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 + ) * 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, + ), ) - 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"] + + 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 { - "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'], - "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, - } + + return response @restapi.method( [ From ab27bdde905d90d2492a9a756a9d8221835b7f6b Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 26 Jun 2023 19:09:58 +0200 Subject: [PATCH 418/547] [FIX] ms-api-rest: add default code field to amenity service payload --- pms_api_rest/services/pms_amenity_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/services/pms_amenity_service.py b/pms_api_rest/services/pms_amenity_service.py index ac6e4a3fdf..161a6edbfd 100644 --- a/pms_api_rest/services/pms_amenity_service.py +++ b/pms_api_rest/services/pms_amenity_service.py @@ -50,6 +50,7 @@ def get_amenities(self, amenities_search_param): 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 From 7a9e64633defc5c8539f43c9c29d4e00f712fd66 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 28 Jun 2023 11:50:21 +0200 Subject: [PATCH 419/547] [FIX] pms-api-rest: round daily billing headers --- pms_api_rest/services/pms_calendar_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index ce6eb5db5f..51bdb6ec67 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -361,7 +361,7 @@ def get_calendar_headers(self, calendar_search_param): f""" SELECT d.date, bool_or(l.overbooking) overbooking, - SUM(l.price_day_total) daily_billing, + CEIL(SUM(l.price_day_total)) daily_billing, tr.num_total_rooms - ( @@ -373,7 +373,7 @@ def get_calendar_headers(self, calendar_search_param): AND occupies_availability = true AND room_id IN %s ) free_rooms, - ceil(( + CEIL(( SELECT COUNT(1) FROM pms_reservation_line l INNER JOIN pms_reservation r ON r.id = l.reservation_id From 07efe1f03cbcdaa196b3714c5b6c9ff15a730595 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 28 Jun 2023 12:22:36 +0200 Subject: [PATCH 420/547] [FIX] pms-api-rest: old calendar reselling False if is None --- pms_api_rest/services/pms_calendar_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 51bdb6ec67..18dfe05482 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -192,7 +192,7 @@ def get_old_calendar(self, calendar_search_param): nextLineSplitted=next_line_splitted, previousLineSplitted=previous_line_splitted, closureReasonId=line["closure_reason_id"], - isReselling=line["is_reselling"], + isReselling=line["is_reselling"] if line["is_reselling"] else False, ) ) return result_lines From 6976f91740c444b6d8be0e93d3f5d44401076020 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 11 Jul 2023 12:13:17 +0200 Subject: [PATCH 421/547] [IMP] pms-api-rest: add restrictions to calendar service --- pms_api_rest/datamodels/pms_calendar.py | 2 + pms_api_rest/services/pms_calendar_service.py | 53 ++++++++++++++++--- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index d40ba56b0a..d065457033 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -28,6 +28,7 @@ class PmsCalendarSearchParam(Datamodel): 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): @@ -59,6 +60,7 @@ class PmsCalendarHeaderInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 18dfe05482..8cd05e3732 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -53,6 +53,25 @@ def build_reservation_line_info( calendar_item, previous_item=False, next_item=F } +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" @@ -237,6 +256,13 @@ def get_calendar_new(self, calendar_search_param): "adults": "r.adults adults", "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) @@ -263,6 +289,11 @@ def get_calendar_new(self, calendar_search_param): WHERE pms_property_id = %s AND state != 'cancel' AND occupies_availability = true AND date <= %s ) 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.room_id, dr.date @@ -273,6 +304,8 @@ def get_calendar_new(self, calendar_search_param): 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() @@ -281,6 +314,16 @@ def get_calendar_new(self, calendar_search_param): 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 @@ -291,19 +334,13 @@ def get_calendar_new(self, calendar_search_param): roomTypeClassId=item["room_type_class_id"], roomTypeId=item["room_type_id"], dates=[ - { - "date": datetime.combine(item['date'], datetime.min.time()).isoformat(), - "reservationLines": [], - } + date ], ) ) else: response[-1].dates.append( - { - "date": datetime.combine(item['date'], datetime.min.time()).isoformat(), - "reservationLines": [], - } + date ) if item['reservation_id'] is not None and item['reservation_id'] != last_reservation_id: response[-1].dates[-1]['reservationLines'].append( From c6fc6f6164aaa3d451609952999a668789bc89b1 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 12 Jul 2023 17:35:10 +0200 Subject: [PATCH 422/547] [IMP]pms-api-rest: added isReselling field to reservation datamodel --- pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/services/pms_folio_service.py | 1 + pms_api_rest/services/pms_reservation_service.py | 1 + 3 files changed, 3 insertions(+) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 98a14b71e2..e396172e60 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -90,6 +90,7 @@ class PmsReservationInfo(Datamodel): ) partnerRequests = fields.String(required=False, allow_none=True) nights = fields.Integer(required=False, allow_none=True) + isReselling = fields.Boolean(required=False, allow_none=True) # TODO: Refact # messages = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 4ab2e00d96..196a0aa5af 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -235,6 +235,7 @@ def get_folios(self, folio_search_param): if reservation.service_ids else 0, "overbooking": reservation.overbooking, + "isReselling": any(line.is_reselling for line in reservation.reservation_line_ids), } ) result_folios.append( diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 12c12812d4..d1c2aabf12 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -127,6 +127,7 @@ def get_reservation(self, reservation_id, pms_search_param): numServices=len(reservation.service_ids) if reservation.service_ids else 0, + isReselling=any(line.is_reselling for line in reservation.reservation_line_ids), ) return res From 42aef907f5d065b6f60b5916c36908dc3399a990 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Jul 2023 10:50:56 +0200 Subject: [PATCH 423/547] [IMP] pms-api-rest: order by sequence and filter by active @ calendar service --- pms_api_rest/services/pms_calendar_service.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 8cd05e3732..d4851d6504 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -274,15 +274,17 @@ def get_calendar_new(self, calendar_search_param): 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.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 + (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 pms_property_id = %s) r_rt_rtc + WHERE r.active = true AND r.pms_property_id = %s) r_rt_rtc ) dr LEFT OUTER JOIN ( SELECT id, state, price_day_total, room_id, date, reservation_id FROM pms_reservation_line @@ -296,7 +298,7 @@ def get_calendar_new(self, calendar_search_param): ) 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.room_id, dr.date + ORDER BY dr.sequence, dr.room_id, dr.date """, ( calendar_search_param.dateFrom, From 87da33cf0557376f47634dae1192fde6c5f79d0e Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Jul 2023 11:56:22 +0200 Subject: [PATCH 424/547] [FIX] pms-pwa: add amenity in room name as a field in calendar service --- pms_api_rest/datamodels/pms_room.py | 1 + pms_api_rest/services/pms_room_service.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pms_api_rest/datamodels/pms_room.py b/pms_api_rest/datamodels/pms_room.py index daff5d12d3..55a4e2ebcb 100644 --- a/pms_api_rest/datamodels/pms_room.py +++ b/pms_api_rest/datamodels/pms_room.py @@ -24,3 +24,4 @@ class PmsRoomInfo(Datamodel): 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/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index a96855b049..60a20daa4f 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -97,6 +97,8 @@ def get_rooms(self, room_search_param): 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).name if + room.room_amenity_ids.filtered(lambda x: x.is_add_code_room_name).name else '' ) ) return result_rooms From 5d768437ab1810382f9f7ccb8fa7cc9a2314e221 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 13 Jul 2023 19:08:29 +0200 Subject: [PATCH 425/547] [FIX] pms-pwa: change field name by default code @ room service in amenityInRoomName --- pms_api_rest/services/pms_room_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 60a20daa4f..5f146cdc0a 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -97,7 +97,7 @@ def get_rooms(self, room_search_param): 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).name if + 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 '' ) ) From a324ac4716cbc9abc4b724ace941fe3903187db5 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 19 Jul 2023 13:37:51 +0200 Subject: [PATCH 426/547] [IMP] pms-api-rest: param for daily pricelists-services & new plannign pricelist service --- pms_api_rest/datamodels/pms_calendar.py | 6 + pms_api_rest/datamodels/pms_pricelist.py | 2 +- pms_api_rest/services/pms_calendar_service.py | 124 +++++++++++++++++- .../services/pms_pricelist_service.py | 12 +- 4 files changed, 136 insertions(+), 8 deletions(-) diff --git a/pms_api_rest/datamodels/pms_calendar.py b/pms_api_rest/datamodels/pms_calendar.py index d065457033..a136074b0d 100644 --- a/pms_api_rest/datamodels/pms_calendar.py +++ b/pms_api_rest/datamodels/pms_calendar.py @@ -99,6 +99,12 @@ class PmsCalendarRenderInfo(Datamodel): 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) diff --git a/pms_api_rest/datamodels/pms_pricelist.py b/pms_api_rest/datamodels/pms_pricelist.py index cd023db489..07189c658f 100644 --- a/pms_api_rest/datamodels/pms_pricelist.py +++ b/pms_api_rest/datamodels/pms_pricelist.py @@ -5,10 +5,10 @@ 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): diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index d4851d6504..2697a149a9 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -91,7 +91,7 @@ class PmsCalendarService(Component): output_param=Datamodel("pms.calendar.info", is_list=True), auth="jwt_api_pms", ) - def get_old_calendar(self, calendar_search_param): + 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 @@ -229,7 +229,7 @@ def get_old_calendar(self, calendar_search_param): output_param=Datamodel("pms.calendar.render.info", is_list=True), auth="jwt_api_pms", ) - def get_calendar_new(self, calendar_search_param): + 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() @@ -375,6 +375,126 @@ def get_calendar_new(self, calendar_search_param): 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( + f""" + 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 + FROM + ( + SELECT dates.date, rt_r.room_type_id, 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.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) + ) 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 + ORDER BY dr.room_type_id, dr.date; + """, + ( + calendar_search_param.pmsPropertyId, + date_from, + date_to, + calendar_search_param.pmsPropertyId, + calendar_search_param.pmsPropertyId, + calendar_search_param.availabilityPlanId, + calendar_search_param.pricelistId, + ), + ) + + 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(), + "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( [ diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index dd5d43ea6d..538cac390e 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -28,11 +28,13 @@ class PmsPricelistService(Component): auth="jwt_api_pms", ) def get_pricelists(self, pms_search_param, **args): - pricelists = self.env["product.pricelist"].search( - [ - ("is_pms_available", "=", True), - ] - ) + 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( _( From e9c0c31770c24daa990b2cd6404d7e3a7e2e0f61 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 21 Jul 2023 18:13:11 +0200 Subject: [PATCH 427/547] [IMP] pms-api-rest: add free rooms by type @ calendar pricelist and rules service --- pms_api_rest/services/pms_calendar_service.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 2697a149a9..c8f1e2c7c6 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -419,7 +419,22 @@ def get_prices_rules_calendar(self, calendar_search_param): AND ipp.record = pp.product_tmpl_id AND ipp.pms_property_id = %s ) - ) price + ) 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 = 1 + 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) + AND EXISTS (SELECT 1 + FROM pms_room + WHERE active = true + AND room_type_id = dr.room_type_id + AND pms_property_id = %s) + ) free_rooms FROM ( SELECT dates.date, rt_r.room_type_id, rt_r.product_id, rt_r.default_max_avail, rt_r.default_quota @@ -436,7 +451,9 @@ def get_prices_rules_calendar(self, calendar_search_param): FROM pms_room_type rt WHERE EXISTS ( SELECT 1 FROM pms_room - WHERE pms_property_id = %s AND room_type_id = rt.id) + 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 @@ -452,6 +469,7 @@ def get_prices_rules_calendar(self, calendar_search_param): ORDER BY dr.room_type_id, dr.date; """, ( + calendar_search_param.pmsPropertyId, calendar_search_param.pmsPropertyId, date_from, date_to, @@ -468,6 +486,7 @@ def get_prices_rules_calendar(self, calendar_search_param): 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'], From f62553ee3142e90af8495cd8ae5f1141ee24a58c Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 24 Jul 2023 16:45:34 +0200 Subject: [PATCH 428/547] [REF] pms-api-rest: maxAvail -> maxAvailability @ calendar service --- pms_api_rest/services/pms_calendar_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index c8f1e2c7c6..7f742a7e52 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -489,7 +489,7 @@ def get_prices_rules_calendar(self, calendar_search_param): "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'], From 5e1268dfa81dcfdbc581b966f19bd06d9907b211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 25 Jul 2023 22:16:12 +0200 Subject: [PATCH 429/547] [IMP]pms_api_rest: keep line data in post reservation with reservation lines --- pms_api_rest/services/pms_folio_service.py | 47 ++++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 196a0aa5af..6cd65d497e 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -153,20 +153,20 @@ def get_folios(self, folio_search_param): [("reservation_type", "!=", "out")], ] domain_filter.append(expression.AND(subdomains)) - elif folio_search_param.filterByState == 'onBoard': + 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': + 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': + elif folio_search_param.filterByState == "cancelled": subdomains = [ [("state", "=", "cancel")], ] @@ -187,7 +187,7 @@ def get_folios(self, folio_search_param): PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] for folio in self.env["pms.folio"].search( - [("id", "in", reservations_result),("reservation_type", "!=", "out")], + [("id", "in", reservations_result), ("reservation_type", "!=", "out")], order="write_date desc", limit=folio_search_param.limit, offset=folio_search_param.offset, @@ -204,11 +204,12 @@ def get_folios(self, folio_search_param): reservation.checkout, datetime.min.time() ).isoformat(), "stateCode": reservation.state, - "cancelledReason": reservation.cancelled_reason if reservation.cancelled_reason else None, + "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, @@ -235,7 +236,10 @@ def get_folios(self, folio_search_param): if reservation.service_ids else 0, "overbooking": reservation.overbooking, - "isReselling": any(line.is_reselling for line in reservation.reservation_line_ids), + "isReselling": any( + line.is_reselling + for line in reservation.reservation_line_ids + ), } ) result_folios.append( @@ -259,7 +263,9 @@ def get_folios(self, folio_search_param): 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, + 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), ) @@ -478,8 +484,8 @@ def get_folio_reservations(self, folio_id): lambda x: not x.is_board_service ).mapped("product_qty") ), - nights= reservation.nights, - numServices= len(reservation.service_ids) + nights=reservation.nights, + numServices=len(reservation.service_ids) if reservation.service_ids else 0, toAssign=reservation.to_assign, @@ -557,8 +563,6 @@ def create_folio(self, pms_folio_info): 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 or "normal", @@ -573,6 +577,25 @@ def create_folio(self, pms_folio_info): "children": reservation.children, "preconfirm": pms_folio_info.preconfirm, } + if reservation.reservationLines: + vals_lines = [] + for reservationLine in reservation.reservationLines: + vals_lines.append( + ( + 0, + 0, + { + "date": reservationLine.date, + "price": reservationLine.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( From 36c831a846bf0668d867bdd41c69ffc81d33c824 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 26 Jul 2023 17:29:08 +0200 Subject: [PATCH 430/547] [FIX] pms-api-rest: fix param property in sql pricelist calendar & fix item union with property --- pms_api_rest/services/pms_calendar_service.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 7f742a7e52..913fbaa530 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -422,7 +422,7 @@ def get_prices_rules_calendar(self, calendar_search_param): ) 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 = 1 + 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 @@ -466,9 +466,13 @@ def get_prices_rules_calendar(self, calendar_search_param): 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.room_type_id, dr.date; """, ( + calendar_search_param.pmsPropertyId, calendar_search_param.pmsPropertyId, calendar_search_param.pmsPropertyId, date_from, @@ -477,6 +481,7 @@ def get_prices_rules_calendar(self, calendar_search_param): calendar_search_param.pmsPropertyId, calendar_search_param.availabilityPlanId, calendar_search_param.pricelistId, + calendar_search_param.pmsPropertyId, ), ) From 15a6ecdb7702808332d45cf5684ca39714f9eb24 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 26 Jul 2023 17:52:31 +0200 Subject: [PATCH 431/547] [FIX] pms-api-rest: remove unnecessary query from subquery for free rooms --- pms_api_rest/services/pms_calendar_service.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 913fbaa530..50a5d40435 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -429,11 +429,6 @@ def get_prices_rules_calendar(self, calendar_search_param): AND occupies_availability = true AND room_id = r.id AND r.is_shared_room = false) - AND EXISTS (SELECT 1 - FROM pms_room - WHERE active = true - AND room_type_id = dr.room_type_id - AND pms_property_id = %s) ) free_rooms FROM ( @@ -472,7 +467,6 @@ def get_prices_rules_calendar(self, calendar_search_param): ORDER BY dr.room_type_id, dr.date; """, ( - calendar_search_param.pmsPropertyId, calendar_search_param.pmsPropertyId, calendar_search_param.pmsPropertyId, date_from, From 0754d126ca76eab5262947b1d896c5c558d5cbf7 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 28 Jul 2023 09:37:04 +0200 Subject: [PATCH 432/547] [FIX] pms-api-rest: fix order by sequence @ calendar pricelist items --- pms_api_rest/services/pms_calendar_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index 50a5d40435..fd755f1faa 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -432,7 +432,8 @@ def get_prices_rules_calendar(self, calendar_search_param): ) free_rooms FROM ( - SELECT dates.date, rt_r.room_type_id, rt_r.product_id, rt_r.default_max_avail, rt_r.default_quota + 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 @@ -440,6 +441,7 @@ def get_prices_rules_calendar(self, calendar_search_param): ) dates, ( SELECT rt.id room_type_id, + rt.sequence, rt.product_id, rt.default_max_avail, rt.default_quota @@ -464,7 +466,7 @@ def get_prices_rules_calendar(self, calendar_search_param): 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.room_type_id, dr.date; + ORDER BY dr.sequence, dr.room_type_id, dr.date; """, ( calendar_search_param.pmsPropertyId, From 46495bc9a5bd8ac06b31381e53df68293654decc Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 28 Jul 2023 10:56:15 +0200 Subject: [PATCH 433/547] [FIX]14.0-pms_api_rest: format date in refund service --- pms_api_rest/services/pms_folio_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 6cd65d497e..1b486f94b4 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -414,7 +414,7 @@ def create_folio_refund(self, folio_id, pms_account_payment_info): reservations=False, services=False, partner=partner_id, - date=datetime.strptime(pms_account_payment_info.date, "%m/%d/%Y"), + date=datetime.strptime(pms_account_payment_info.date, "%Y-%m-%d"), ref=pms_account_payment_info.reference, ) From 7cca31018788e72afef7f69d15c5e2b0443aceeb Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 7 Aug 2023 16:07:17 +0200 Subject: [PATCH 434/547] WIP --- pms_api_rest/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/http.py b/pms_api_rest/http.py index 762d36c674..e7b165367f 100644 --- a/pms_api_rest/http.py +++ b/pms_api_rest/http.py @@ -49,7 +49,7 @@ def _handle_exception(self, exception): return wrapJsonException( Forbidden(ustr(e)), include_description=True, extra_info=extra_info ) - except (UserError, ValidationError) as e: + 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 From 3b1fb5df0c0318ae82b0ac42c393df47c8803bc6 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 8 Aug 2023 12:10:34 +0200 Subject: [PATCH 435/547] [IMP] pms-api-rest: add min price to room type service --- pms_api_rest/datamodels/pms_room_type.py | 1 + pms_api_rest/services/pms_room_type_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_room_type.py b/pms_api_rest/datamodels/pms_room_type.py index b96b869eb0..301672e7b2 100644 --- a/pms_api_rest/datamodels/pms_room_type.py +++ b/pms_api_rest/datamodels/pms_room_type.py @@ -17,5 +17,6 @@ class PmsRoomTypeInfo(Datamodel): 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/services/pms_room_type_service.py b/pms_api_rest/services/pms_room_type_service.py index d2205cad17..f78749d9ae 100644 --- a/pms_api_rest/services/pms_room_type_service.py +++ b/pms_api_rest/services/pms_room_type_service.py @@ -60,6 +60,7 @@ def get_room_types(self, room_type_search_param): 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, From 3f0ca467241f87be27c9f2f345a5b19dabdd84fd Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 20 Jul 2023 20:40:28 +0200 Subject: [PATCH 436/547] [IMP]pms_api_rest: added get address by display_name service --- pms_api_rest/datamodels/res_city_zip.py | 6 ++++ pms_api_rest/services/res_city_zip_service.py | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/pms_api_rest/datamodels/res_city_zip.py b/pms_api_rest/datamodels/res_city_zip.py index 36a54f2cad..d3569ae8c3 100644 --- a/pms_api_rest/datamodels/res_city_zip.py +++ b/pms_api_rest/datamodels/res_city_zip.py @@ -3,8 +3,14 @@ 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/services/res_city_zip_service.py b/pms_api_rest/services/res_city_zip_service.py index 92ad714983..195045dbb0 100644 --- a/pms_api_rest/services/res_city_zip_service.py +++ b/pms_api_rest/services/res_city_zip_service.py @@ -9,6 +9,42 @@ class ResCityZipService(Component): _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="jwt_api_pms", + ) + 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"].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( [ ( From b92bdc68c3977db3a1177a4bd2a251cd2f5cb116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 13 Aug 2023 10:11:53 +0200 Subject: [PATCH 437/547] [FIX]pms_api_rest: get invoice with multi folios in list --- pms_api_rest/services/pms_invoice_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 196f18d298..0828757617 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -165,7 +165,7 @@ def get_invoices(self, pms_invoice_search_param): 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, + folioId=invoice.folio_ids[0] if invoice.folio_ids else None, portalUrl=portal_url, moveType=invoice.move_type, isReversed=invoice.payment_state == "reversed", From 40c5b41c92c1c6d90d7e4ee98205ecf3814ac77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 13 Aug 2023 10:15:54 +0200 Subject: [PATCH 438/547] [ADD]pms_api_rest: POST/PATCH folio payments --- pms_api_rest/services/pms_folio_service.py | 71 +++++++++++++++++++++- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 1b486f94b4..1f594fd802 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -507,6 +507,7 @@ def get_folio_reservations(self, folio_id): input_param=Datamodel("pms.folio.info", is_list=False), auth="jwt_api_pms", ) + # flake8:noqa=C901 def create_folio(self, pms_folio_info): call_type = self.get_api_client_type() if pms_folio_info.reservationType == "out": @@ -599,7 +600,9 @@ def create_folio(self, pms_folio_info): reservation_record = ( self.env["pms.reservation"] .with_context( - skip_compute_service_ids=True, + skip_compute_service_ids=False + if call_type == "external_app" + else True, force_overbooking=True if call_type == "external_app" else False, ) .create(vals) @@ -639,11 +642,14 @@ def create_folio(self, pms_folio_info): "product_qty": service.quantity, } ) - self.env["pms.service"].create(vals) + 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: reservation_record._compute_board_service_room_id() + 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 # & @@ -664,6 +670,65 @@ def create_folio(self, pms_folio_info): template.send_mail(folio.id, force_send=True, email_values=email_values) return folio.id + def compute_transactions(self, folio, transactions): + """ + "transactions": [ + { + "transactionReference": "De34deaDea43242", + "amount": 32.94, + "date": "2023-05-17", + "transactionType": "inbound" + }, + ... + ], + """ + for transaction in transactions: + reference = folio.name + " - " + if transaction.transactionReference: + reference += transaction.transactionReference + else: + raise ValidationError(_("The transaction reference is required")) + if not self.env["account.payment"].search( + [ + ("pms_property_id", "=", folio.pms_property_id.id), + ("payment_type", "=", transaction.transactionType), + ("folio_ids", "in", folio.id), + ("ref", "ilike", transaction.transactionReference), + ] + ): + # TODO: Move this to the user API payment configuration + journal = ( + self.env["channel.wubook.backend"] + .search([("pms_property_id", "=", folio.pms_property_id.id)]) + .wubook_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, "%m/%d/%Y"), + ref=reference, + ) + @restapi.method( [ ( @@ -676,6 +741,7 @@ def create_folio(self, pms_folio_info): 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 = {} @@ -1091,7 +1157,6 @@ def create_folio_invoices(self, folio_id, invoice_info): 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 From d577f71b5ae7941f2345a12e63d3fdd4ebad5de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 14 Aug 2023 09:50:18 +0200 Subject: [PATCH 439/547] [ADD]pms_api_rest: add otas parameter in get agency service --- pms_api_rest/datamodels/pms_agency.py | 1 + pms_api_rest/services/pms_agency_service.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pms_api_rest/datamodels/pms_agency.py b/pms_api_rest/datamodels/pms_agency.py index bd4de19e9f..72ce4ce588 100644 --- a/pms_api_rest/datamodels/pms_agency.py +++ b/pms_api_rest/datamodels/pms_agency.py @@ -6,6 +6,7 @@ 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): diff --git a/pms_api_rest/services/pms_agency_service.py b/pms_api_rest/services/pms_agency_service.py index 9d33cd95e6..d6895b5653 100644 --- a/pms_api_rest/services/pms_agency_service.py +++ b/pms_api_rest/services/pms_agency_service.py @@ -27,6 +27,8 @@ class PmsAgencyService(Component): ) 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 = [] From 33d5c5f954cdd8855e38453838124b0123abce71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 14 Aug 2023 09:51:35 +0200 Subject: [PATCH 440/547] [ADD]pms_api_rest: add transaction in folio POST/PATCH --- pms_api_rest/datamodels/pms_folio.py | 3 +++ pms_api_rest/services/pms_folio_service.py | 22 ++++++---------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index f94b8bd4b5..76389921fb 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -48,6 +48,9 @@ class PmsFolioInfo(Datamodel): invoiceStatus = fields.String(required=False, allow_none=True) portalUrl = 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): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 1f594fd802..15d847ebd1 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -671,21 +671,10 @@ def create_folio(self, pms_folio_info): return folio.id def compute_transactions(self, folio, transactions): - """ - "transactions": [ - { - "transactionReference": "De34deaDea43242", - "amount": 32.94, - "date": "2023-05-17", - "transactionType": "inbound" - }, - ... - ], - """ for transaction in transactions: reference = folio.name + " - " - if transaction.transactionReference: - reference += transaction.transactionReference + if transaction.reference: + reference += transaction.reference else: raise ValidationError(_("The transaction reference is required")) if not self.env["account.payment"].search( @@ -693,7 +682,7 @@ def compute_transactions(self, folio, transactions): ("pms_property_id", "=", folio.pms_property_id.id), ("payment_type", "=", transaction.transactionType), ("folio_ids", "in", folio.id), - ("ref", "ilike", transaction.transactionReference), + ("ref", "ilike", transaction.reference), ] ): # TODO: Move this to the user API payment configuration @@ -725,7 +714,7 @@ def compute_transactions(self, folio, transactions): reservations=False, services=False, partner=False, - date=datetime.strptime(transaction.date, "%m/%d/%Y"), + date=datetime.strptime(transaction.date, "%Y-%m-%d"), ref=reference, ) @@ -1516,7 +1505,8 @@ def update_folio_values(self, folio, pms_folio_info): skip_compute_service_ids=True, force_overbooking=True if call_type == "external_app" else False, ).write(folio_vals) - _logger.info("FOLIO VALS", folio_vals) + if pms_folio_info.transactions: + self.compute_transactions(folio, pms_folio_info.transactions) def wrapper_reservations(self, folio, info_reservations): """ From bd83fb3dc53d662a01f81dc64dec597fbfaac53c Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 15 Sep 2023 21:16:22 +0200 Subject: [PATCH 441/547] [IMP]14.0-pms_api_rest: added residence state name in checkin partner datamodel --- pms_api_rest/datamodels/pms_checkin_partner.py | 1 + pms_api_rest/services/pms_reservation_service.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py index cbab413297..6fb5924733 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner.py +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -24,6 +24,7 @@ class PmsCheckinPartnerInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index d1c2aabf12..20ff02a7dd 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -596,6 +596,9 @@ def get_checkin_partners(self, reservation_id): 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, From f9808c9e9a5044eaf6267fef9590b270e391a6dc Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 21 Sep 2023 12:30:00 +0200 Subject: [PATCH 442/547] [IMP]pms_api_rest: added check vat service and ref get partner by doc service --- .../datamodels/pms_checkin_partner.py | 1 + pms_api_rest/services/pms_partner_service.py | 204 ++++++++++++------ 2 files changed, 143 insertions(+), 62 deletions(-) diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py index 6fb5924733..c068422f22 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner.py +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -6,6 +6,7 @@ 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) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 662eddb7a7..f6cdc651cf 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -6,8 +6,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 odoo.exceptions import ValidationError +from odoo import _ - +_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" @@ -308,82 +361,89 @@ def get_partner_by_doc_number(self, document_type, document_number): [("id", "=", document_type)] ) # Clean Document number - doc_number = False 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 ) - if partner.id_numbers: - doc_number = partner.id_numbers[0] - partners = [] - PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] - if not doc_number: - pass - else: + if partner: + doc_number = partner.id_numbers.filtered( + lambda doc: doc.category_id.id == doc_type.id + ) + + + PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] + + document_expedition_date = False if doc_number.valid_from: document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") - if doc_number.partner_id.birthdate_date: - birthdate_date = doc_number.partner_id.birthdate_date.strftime( + birthdate_date = False + if partner.birthdate_date: + birthdate_date = partner.birthdate_date.strftime( "%d/%m/%Y" ) partners.append( PmsCheckinPartnerInfo( - id=doc_number.partner_id.id, - name=doc_number.partner_id.name - if doc_number.partner_id.name - else None, - firstname=doc_number.partner_id.firstname - if doc_number.partner_id.firstname - else None, - lastname=doc_number.partner_id.lastname - if doc_number.partner_id.lastname - else None, - lastname2=doc_number.partner_id.lastname2 - if doc_number.partner_id.lastname2 - else None, - email=doc_number.partner_id.email - if doc_number.partner_id.email - else None, - mobile=doc_number.partner_id.mobile - if doc_number.partner_id.mobile - else None, - documentType=doc_type.id, - documentNumber=doc_number.name, - documentExpeditionDate=document_expedition_date - if doc_number.valid_from and doc_number.category_id == doc_type - else None, - documentSupportNumber=doc_number.support_number - if doc_number.support_number and doc_number.category_id == doc_type - else None, - gender=doc_number.partner_id.gender - if doc_number.partner_id.gender - else None, - birthdate=birthdate_date - if doc_number.partner_id.birthdate_date - else None, - residenceStreet=doc_number.partner_id.residence_street - if doc_number.partner_id.residence_street - else None, - zip=doc_number.partner_id.residence_zip - if doc_number.partner_id.residence_zip - else None, - residenceCity=doc_number.partner_id.residence_city - if doc_number.partner_id.residence_city - else None, - nationality=doc_number.partner_id.nationality_id.id - if doc_number.partner_id.nationality_id - else None, - countryId=doc_number.partner_id.residence_country_id - if doc_number.partner_id.residence_country_id - else None, - countryState=doc_number.partner_id.residence_state_id.id - if doc_number.partner_id.residence_state_id - else None, + 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_number.name or None, + documentExpeditionDate=document_expedition_date or None, + documentSupportNumber=doc_number.support_number or None, + gender=partner.gender or None, + birthdate=birthdate_date or 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="jwt_api_pms", + ) + # 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'].browse(country_id) + document_type = self.env['res.partner.id_category'].browse(document_type_id) + id_number = self.env["res.partner.id_number"].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.aeat_identification_type in ["02", "04"]: + 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( [ ( @@ -581,3 +641,23 @@ def mapping_partner_values(self, pms_partner_info): 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 + ) From e188fce73aa39f1dbc92ade053921bbb33f75fa1 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 21 Sep 2023 12:33:12 +0200 Subject: [PATCH 443/547] [IMP]pms_api_rest: new field code in document types datamodel --- pms_api_rest/datamodels/pms_id_category.py | 1 + pms_api_rest/services/pms_id_category_service.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_id_category.py b/pms_api_rest/datamodels/pms_id_category.py index a802071f0a..749fb38721 100644 --- a/pms_api_rest/datamodels/pms_id_category.py +++ b/pms_api_rest/datamodels/pms_id_category.py @@ -7,3 +7,4 @@ 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) diff --git a/pms_api_rest/services/pms_id_category_service.py b/pms_api_rest/services/pms_id_category_service.py index 57db6a43b8..80c50577ad 100644 --- a/pms_api_rest/services/pms_id_category_service.py +++ b/pms_api_rest/services/pms_id_category_service.py @@ -24,11 +24,12 @@ class PmsIdCategoryService(Component): 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"].search([]): + for id_category in self.env["res.partner.id_category"].with_context(lang=self.env.user.lang).search([]): result_id_categories.append( PmsIdCategoryInfo( id=id_category.id, documentType=id_category.name, + code=id_category.code, ) ) return result_id_categories From ac7ccdc5c42eb24d3537fd2fd74a09ee44cf993b Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 21 Sep 2023 12:35:57 +0200 Subject: [PATCH 444/547] [IMP]pms_api_rest: added user language to countries and segmentations --- pms_api_rest/services/res_country_service.py | 2 +- pms_api_rest/services/res_partner_category_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/res_country_service.py b/pms_api_rest/services/res_country_service.py index ea6d7b194e..206d0cb3fe 100644 --- a/pms_api_rest/services/res_country_service.py +++ b/pms_api_rest/services/res_country_service.py @@ -24,7 +24,7 @@ class ResCountryService(Component): def get_countries(self): result_countries = [] ResCountriesInfo = self.env.datamodels["res.country.info"] - for country in self.env["res.country"].search([]): + for country in self.env["res.country"].with_context(lang=self.env.user.lang).search([]): result_countries.append( ResCountriesInfo( id=country.id, diff --git a/pms_api_rest/services/res_partner_category_service.py b/pms_api_rest/services/res_partner_category_service.py index 34f8af8d45..ee3efc3717 100644 --- a/pms_api_rest/services/res_partner_category_service.py +++ b/pms_api_rest/services/res_partner_category_service.py @@ -24,7 +24,7 @@ class PmsPartnerCategoriesService(Component): def get_categories(self): result_categories = [] ResPartnerCategoryInfo = self.env.datamodels["res.partner.category.info"] - for category in self.env["res.partner.category"].search([]): + for category in self.env["res.partner.category"].with_context(lang=self.env.user.lang).search([]): result_categories.append( ResPartnerCategoryInfo( id=category.id, From dc62f6875f177aed856edf0692a63eeb89e70922 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 21 Sep 2023 20:47:36 +0200 Subject: [PATCH 445/547] [IMP]pms_api_rest: if segmentationId equal to 0 it is deleted --- pms_api_rest/services/pms_reservation_service.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 20ff02a7dd..ccc767817d 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -146,10 +146,13 @@ def _create_vals_from_params(self, reservation_vals, reservation_data, reservati 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: - reservation_vals.update( - {"segmentation_ids": [(6, 0, [reservation_data.segmentationId])]} - ) + 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: From 996160621132653465d36002436891833f1dd3b0 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 17 Aug 2023 18:20:16 +0200 Subject: [PATCH 446/547] [FIX] pms-api-rest: fix service av. plan rules & pricelist items with proper id --- .../services/pms_availability_plan_service.py | 56 +++++++++++----- .../services/pms_pricelist_service.py | 64 +++++++++++++++---- 2 files changed, 91 insertions(+), 29 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 7f6370c29b..b96606fbc8 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta -from odoo.exceptions import MissingError +from odoo.exceptions import MissingError, ValidationError from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -183,21 +183,7 @@ def get_availability_plan_rules( return result - @restapi.method( - [ - ( - [ - "//availability-plan-rules", - ], - "POST", - ) - ], - 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 - ): + 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() @@ -239,3 +225,41 @@ def create_availability_plan_rule( } ) self.env["pms.availability.plan.rule"].create(vals) + + @restapi.method( + [ + ( + [ + "//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_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 538cac390e..40378ff0bc 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -143,19 +143,7 @@ def get_pricelists_items(self, pricelist_id, pricelist_item_search_param): result.append(pricelist_info) return result - @restapi.method( - [ - ( - [ - "//pricelist-items", - ], - "POST", - ) - ], - input_param=Datamodel("pms.pricelist.items.info", is_list=False), - auth="jwt_api_pms", - ) - def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): + 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 = ( @@ -191,3 +179,53 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): "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(self, pricelist_id, pms_pricelist_item_info): + self._create_or_update_pricelist_items(pms_pricelist_item_info) + + @restapi.method( + [ + ( + [ + "//pricelist-items", + ], + "PATCH", + ) + ], + input_param=Datamodel("pms.pricelist.items.info", is_list=False), + auth="jwt_api_pms", + ) + def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): + pricelist_ids = list({item.pricelistId for item in pms_pricelist_item_info.pricelistItems}) + print(pricelist_ids) + 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) From d8aa915e1fd23e69d05b5990e4e73fb5f1c1117b Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 18 Aug 2023 12:34:59 +0200 Subject: [PATCH 447/547] FIX] pms-api-rest: fix post->patch @ service batch changes in av. plan rules & pricelist items services --- pms_api_rest/services/pms_availability_plan_service.py | 2 +- pms_api_rest/services/pms_pricelist_service.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index b96606fbc8..2ef5c3b963 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -255,7 +255,7 @@ def create_availability_plan_rule(self, availability_plan_id, pms_avail_plan_rul [ "/batch-changes", ], - "POST", + "PATCH", ) ], input_param=Datamodel("pms.availability.plan.rules.info", is_list=False), diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 40378ff0bc..bcbb79319d 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -209,7 +209,6 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): ) def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): pricelist_ids = list({item.pricelistId for item in pms_pricelist_item_info.pricelistItems}) - print(pricelist_ids) if len(pricelist_ids) > 1 or pricelist_ids[0] != pricelist_id: raise ValidationError("You cannot create pricelist items for different pricelists at once.") else: @@ -221,7 +220,7 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): [ "/batch-changes", ], - "POST", + "PATCH", ) ], input_param=Datamodel("pms.pricelist.items.info", is_list=False), From ec00f1069b75182ba46b23cecb4169eebad72e62 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 6 Sep 2023 19:22:21 +0200 Subject: [PATCH 448/547] [IMP] pms-api-rest: demo data for property image hotel, icon room type classes, views, init, manifest & services & datamodels (property, user, room_type_class, agency, sale channels) --- pms_api_rest/__manifest__.py | 1 + pms_api_rest/datamodels/pms_agency.py | 2 +- pms_api_rest/datamodels/pms_property.py | 1 + .../datamodels/pms_room_type_class.py | 1 + pms_api_rest/datamodels/pms_sale_channel.py | 2 +- pms_api_rest/datamodels/pms_user.py | 1 + .../demo/pms_api_rest_master_data.xml | 15 +++++++ ...y_hotel_image_pms_api_rest_my_property.jpg | Bin 0 -> 129939 bytes ...ty_hotel_image_pms_api_rest_san_carlos.jpg | Bin 0 -> 125206 bytes ...ype_class_icon_pms_api_rest_conference.svg | 38 ++++++++++++++++++ ...m_type_class_icon_pms_api_rest_parking.svg | 26 ++++++++++++ ...room_type_class_icon_pms_api_rest_room.svg | 3 ++ pms_api_rest/models/__init__.py | 2 + pms_api_rest/models/pms_property.py | 5 +++ pms_api_rest/models/pms_room_type_class.py | 10 +++++ pms_api_rest/services/manage_url_images.py | 19 +++++++++ pms_api_rest/services/pms_agency_service.py | 7 ++-- pms_api_rest/services/pms_login_service.py | 2 + pms_api_rest/services/pms_property_service.py | 2 + .../services/pms_room_type_class_service.py | 2 + .../services/pms_sale_channel_service.py | 5 +-- pms_api_rest/views/pms_property_views.xml | 5 +++ .../views/pms_room_type_class_views.xml | 12 ++++++ 23 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 pms_api_rest/demo/pms_property_hotel_image_pms_api_rest_my_property.jpg create mode 100644 pms_api_rest/demo/pms_property_hotel_image_pms_api_rest_san_carlos.jpg create mode 100644 pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_conference.svg create mode 100644 pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_parking.svg create mode 100644 pms_api_rest/demo/pms_room_type_class_icon_pms_api_rest_room.svg create mode 100644 pms_api_rest/models/pms_room_type_class.py create mode 100644 pms_api_rest/services/manage_url_images.py create mode 100644 pms_api_rest/views/pms_room_type_class_views.xml diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 9120e04e56..1f338e16c8 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -25,6 +25,7 @@ "data/pms_app_reset_password_template.xml", "views/pms_property_views.xml", "views/res_users_views.xml", + "views/pms_room_type_class_views.xml", ], "demo": [ "demo/pms_api_rest_master_data.xml", diff --git a/pms_api_rest/datamodels/pms_agency.py b/pms_api_rest/datamodels/pms_agency.py index 72ce4ce588..1c519997d2 100644 --- a/pms_api_rest/datamodels/pms_agency.py +++ b/pms_api_rest/datamodels/pms_agency.py @@ -13,4 +13,4 @@ class PmsAgencyInfo(Datamodel): _name = "pms.agency.info" id = fields.Integer(required=True, allow_none=False) name = fields.String(required=True, allow_none=False) - image = fields.String(required=False, allow_none=True) + imageUrl = 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 index df574573a5..caf3b49022 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -29,3 +29,4 @@ class PmsPropertyInfo(Datamodel): 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) diff --git a/pms_api_rest/datamodels/pms_room_type_class.py b/pms_api_rest/datamodels/pms_room_type_class.py index 4498951d73..b10df266fe 100644 --- a/pms_api_rest/datamodels/pms_room_type_class.py +++ b/pms_api_rest/datamodels/pms_room_type_class.py @@ -15,3 +15,4 @@ class PmsRoomTypeClassInfo(Datamodel): 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 index 57d1ecd412..730bb8037d 100644 --- a/pms_api_rest/datamodels/pms_sale_channel.py +++ b/pms_api_rest/datamodels/pms_sale_channel.py @@ -13,4 +13,4 @@ class PmsSaleChannelInfo(Datamodel): id = fields.Integer(required=True, allow_none=False) name = fields.String(required=True, allow_none=False) channelType = fields.String(required=True, allow_none=True) - icon = fields.String(required=False, allow_none=True) + iconUrl = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index 611c715303..63f9289927 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -23,6 +23,7 @@ class PmsApiRestUserOutput(Datamodel): 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) diff --git a/pms_api_rest/demo/pms_api_rest_master_data.xml b/pms_api_rest/demo/pms_api_rest_master_data.xml index bca00d7173..555f68eb8b 100644 --- a/pms_api_rest/demo/pms_api_rest_master_data.xml +++ b/pms_api_rest/demo/pms_api_rest_master_data.xml @@ -13,5 +13,20 @@ eval="[(6, 0, [ref('pms.field_pms_availability_plan_rule__min_stay'), ref('pms.field_pms_availability_plan_rule__quota')])]" /> + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..9224272068c5434da40ccc40d2e3ad0b06eca917 GIT binary patch literal 129939 zcmbSybx<5Z_w6q3PO^kWmjowxu*DMGonXN&Kv>)*1PEkVbV-5}+=F|7Ad7o&2(Y-j zLmuDnS=IaNy?1A(YO1DdYEF0G>2vQr{cq;qGJs53UP&GR!~g(*j|br2Jm56|4+jSq z2OAF;7Z)ENkAR4Zn23;&h=zial#1>d13ld{Fqn~rhn10;3jzkSy<+F$mgdqGGS#$jK`xDkggL8zBRJ4wz0LdcW`v~@bvQb@%0P)6dn=zIVw6KF$tOc_1pK9A33>s`2~eV#Z}cc zwRQCkjZMEhySjT&y?y=T6O(_Yre|j7R#wq#>l>R}+dIc6r)TFEmsi&}|G@=(#Q8t% zzXSU}aFIXa!ob49#KQd#E+B^YMPZ~nO=eGR#Ua%kq*$*65ptK#+ z2~Qh3x4yw%FpGzWuRe&sII#77N;$LOP|)i+3#ZBHR+oomcW?&oee`RFIqDOA3=N5}skLAe<{+Zo=zzw%J zx}_R^_PXydIyf3KoV3(aaFpgRA@t#4j9Y1ysaf43`kF}h@JmoZftnF6D^`YC6t6@; zvz)tj$h+uyxxsO!MsHGDW=CiaaK1FLe0+uqMA%#%8PsN+H~ zU$8nS!FMjkPXx|Ka@AD*Xl zTd;xewuf9lPna#eOqM?h(p2Yc`q^M*@w{vR)3x-I-Bq~nxBj#nle3>5NT}J`ki}+~ z@Ljp6wzz&qOb{*aFfg8k&J;n~M(0r1O8`eew?FJX=cu3hyPLCv-*dc$TL$Ysv-9`| z_)Z6Z-y{?tT}2YZIRES)pecRFjR1*^!n&V*K@)t1;Z-U{x)Z<1%xZoi^LK&w3`*M_-E)I;8XXMQJ7b4-kiJyFL5P;E+by zd|A={58yMth57J8`1;{0U%>LfRpYKM=;=LLE6~Zlj*HXW>L6BbS0@LHW(7K;qv!VY z*TRiwktK^88Qa6{d9hK#0D*fyLh9EBJYxGhY0=jv)HsA3$wHg^=KYn z)mm)@2~3_>JTZLJZC)q9HWO9>9r2r(czCuo$Im?-B_w1n%9lM?a4eD8uK&yF<@1`F zqb0=XTsNC-GvDWq%}R-s!jmG?=Bnp7V}B25+Zi(gMM?(Xv1{0N3Qu_7e&I#$8j~%{ zi>yW0sD6%a6j7)82k^_PUTDhe<YJdE+S?9_g5vIi5P89|K^N0sJss|-e07ONz@P263oFddjjz{gL;-zQj z6Vyb&n1APTe^6wl)wyZVPagbzsXB2&hz8}3h>TEmTfnIYExx85IgDejr+!G0?sBNu-1#ckAN*Vn#`;<0ux^c_lNJKiv2L*Km^Ybjf0xu(K^6;(T6n=1Rj;Y3H2zXr!=WNJs-j1R?}PsQ8n zj6A){6gW#9nu?kEve=ka6tQPa>P zqfC(mV*}}Bfg>;YvYvY{rN>yvBvadUc3D^c5{Uow>O8Gb5t?5I*SgYBje2F%b_`B{7BEkxr#JcwYn^6Y;( z{ixL2BX-Y*BcF{7HQ{C55ZzV_)s&cl42-z+dyn5UU$=_&a7M&Z0Ui6Nh)667{2m2IYgkp~bJ@_IQNMqHZ~#YxD8Wh( zuk zT1=4VtB(eRfB&xP1ca(QBE(uFug_j^Fm{8dS>JDfE#cu`==|*}jLcsS@Elp`&7%f^ zLxiBWB)CH%Dw(Pt^hw!@I+0w!KCXuRNMfFG#a`BEY)cAZ^~z6}>y|iT?mCLHaU}RI zxOD#jz0_nn7vC0Tu!;_+=jU==qlb8g27}DLXFUU-uw8p0onSS`DKS<3RLG?QHV_*3 zD9_f4&;_25bLD@}{#_T#737{Aa4bzaGt()$S!gDj>!t)mx{~G-k0uE zb!O#JY~S8LyLSjBZsN$k{|kdBYEx-S%8QIF4H4r3_8^nh@QrT~*D@a|@4#_>Xu&Gc zShvDqUo)|>7GP~EaXX8)b6iiUx{A504+=em!GsVMcD7~%d5d;qeM4*Ffxs>s8O`68 z6+8S3G0P|_3FG_dVs%t$z>y!_PsvfOastKHIE&36b5Ec+_7=;bF+ zs6CGFNX%2)64(LTRe-l6CCS4(Z@c(ldYU zX-e$4*kr!T)T46ut{`#6HQ_n=7hmJXt=1WoPv@KKlf6=QtYG4vTyEID=auSadn9ur z?{|h`th%2?)DQ?pObt|FUbgl8ygLfXh1ee~4(TTtAnW-?3bOKRLKRxw=1atXBe>fSB6G`w*kU76i+XlnL#rPo={wuuwvC8(^-v6qK4A~R~m=i}8)`i_lK z?|dwkpHrV&w>JD~mP~AawP$G4!gTkr$xP%+fnOYB5<439TlI(wr~tG1Z8C&LOnFB5?(Ixr(a+wI1Z-I>Gy3k z`I}Psw+g?Fr^)9Mepz^E_#1!kL%a1X^DCxE<6PP9|UyeeLcO77yUC zs~4(6O2_29@I-MbUESxqQETGXwB-XppN7Qdv*cUSON1Ea4MUm7oZQV{V{CXrms=ds z(@R?+#%S?xMvJ2oPP|8*zvCY!F3Y94GU)$cas5_#B^uN59QT&xk~#ln4z0fTmbX&a zZSHluM7N7;&iy9zy>l75q58xo#zCDI;*AaVuZCB^~Twd~--0RqR;LFz1~t z{OtYWFl!veK+??zaUs7o#P55>t@2E|8c zcT*<++DmK;%#y&Xa8pS49aEtN23>Za~R`+j(};eO?Hr{MkxWkwkw6hb9vH|zXObrdsT?F)eO2!aP=4UWz6dm_HF(xNup>KQ-0sW3Wj^j z*Qb1t0-5j<$>7iDPM@csJQ%KqcPE*sTBq6VCl`=`n!0EqfORoiC`jD3Ae%v1)Hi*` zl$$)%W<^Eu#p$L&Q)*Gl@}bq)S=p^3MpHRyJ+)Y93tEJl5yT2eLH+;GCW)Gpc)TB_-?%i-rlrvlHR77Sw8oIZU|-BP8;+&xJen zlmx;1zEiqlrTsAJaV)83=yg_D0_=^FR^#~X+}0WO56cJxT*Y?7>cy^h1MZLOZ{ z2z(lyxYi*>9jBtO(Ba^o6yV?|yR`lN6l*s&q}7}OM8bobcd*be5h0TA>zjl;;0-LA zdClN(TiSR5)=b{JtE6@BArUDk#g@k#u!Dog`4WU3_NvX8n;Y<}Cue3bNWZTS1Q85zEa5*5r}NDvFR_rumRU@RH~i{4%uh0& z>qdIboQq#mF_d$Y8mc|!!Z{Qfn#9W))$ooGK+M#Ko0uQbIE9WP zYL3>^5)}6{m6d?R@JvgQZa%W!fqSvv80B;tOQtQp6YAgV%h^M)}~lS#J)H#r3y8wJs8lhA&2tfPQJ9HykxL9N(!f9m6Jd^CU`xm^z-HQ6k@Pz#DDh*E)>2mdrHMjZ z{}WYfgrWIr>89&o8X8hi_7qgbv$hEj~ShuCAlfJtkmzb$A z8eCRO$%FxgyvB^2?hnDge>8Y1F_JX961u@=CI_UFGmQH&m(%Qs?1AHWf80MR6D`x% znqXU-lpCjaO}0~bQBGq$`&?tY$s!<3Dmo0Uock8ZoDj4cKK%6W?i>3q{x%RxxqR?5 zd5J=+TAx1DwzjBc_W)V0Im2s3yf?3DhzwDs;8Qb@xj(R$bKnta`IA70Gn zk!M2)PhX+cmj?(F1ZoPk;9mJk7;uZ6fulhDhnn1KteAQ-6wG-BGKWN4M&Dku#=;&1 zDJ{y#U&n-2&X71Vf`3}dqB2#Z6O*)c`-lD5B?$Btb7CK-Z=9Ibwknsj6{~CKGWY-| zCQA~AgoLK0P3|asMbVrUE@`mSw3~RdXu)QrL$Z2g3)CIC*CUjPI~z8sE;gPcU%}4^ zRo94%@!dp!tXPe-1kx|6cuS!4f*8O%2_FS+f01x%tYT0W%8S`@3|$2@*RqRt%=nZY zN!g<_kH)r-V;6NpgWefgtK6pP_Fdvqmn^LRoCF!U!OI=oLuF5LqF=wpNNitz{QrI~ zVBA%c5Y25@U=S(XFfUY+(6MVkFf}O<s>2V`MbOOR$xIu)i&cbv}4m9 z9O9T%{lwB_E#-7mTaUgVTltekBTIF-T0C(M__XiNF&WmEo8WZRa?i1mnPc*|fvZ_( za&q>$d$#GGw&tniOb5%<#n+W6J5FvC;db;w{lwWl1$OdE5+B$vagUQ#>zE~xJ^C;2 zYdDzRFwkFLJ;(1&EmgfxUh~A6;UJ7H+2W3KG*HcwPTTpl)+ZHK>~=gcl0Z(3_p9nu z8?`Mj^UdBlPEGhK^uGmiEs)ftiCV5RiHh3CI!ZCK(It_NS9?l2&(-9Lr$uKM@|ZLK zlB7S^+wpd$9I91+Zt+&+y2RLE^+aY1_7=K(J}6epRsmTlZvK;a%38l6lVa0;k0$T{ zE4LYW_FEc_8|Bc>Svl2C)qa|n5QW<);by$29^SN?&bW6}0#GVKcO>Y4G&qcXVp-7N ztR7*PF|k0m@_kLaDAH)fB4RN5`?ZKiE?szBrFkpoGx2pVYXNvVf9rDFHJb&~?WlLC z2J>*=B={5e^iYbe)=4=KT5U?bUp+4}$*6;5Xkl2Q-mtXuJxQNrz7|Cc;U(~lGvCC- zRr~``>(#1&YSo}C@dFh{il_kuWGAPwxXCMzZjInT?;8or_bR}}JEOW&qA1$Le*mcM zKftVhqgljl+EP@ZiiO|QD+?1;`vS={-8X`BV_RZpN_H%2Tt;()l-(xxcgAItHp#1e zNy++ihQS_eW$&VoLp9irh7xK#q-Q2@bEFQ=_g1UME zf(I1;0lEw(P?bTUGOT5J&4mFdx&301WxDwf&tMKs0u?+~@#EYS|IlpK~c&!QJ`DTN)G z;o6QYL(K4flZ_dMi*C#K1~_J>p9Nu^LDGyJlRBwzp;Eb~3>nWuraaFw0u6>BiHC^CWd;Vwbr25;+JH9GZ0hr*! zg>YU`+D=MX4W#r5+A@&T-RQzdF8!mg>ua#IACcXz@=)>_|Io;1A*p2SSe1GRFm>}Q z{bIP;#2u$&LSVUP!bQ+44fj*Bz!D4Q!C*dsS9=+!AidddZ`gumkqmzSJk73Cv5#4c zH9_5%aGmkI1m;Gqc8O>?sTV-n$X5N|%{F#e;s7Q3$uKTge3^aqL|0fwWEl@2C@XYV zRaL8sku00>h@ZXlAf(lO>APT_3ES|>FqA{DSJTxew@Wo|v2w<hxU5HKsVoDNNT2_tsIBBG{Ew>hPJR z+&%i|kT{n-FKk*gS5%)q@+Td;w4}MHh+w;(&PT&dsDM(VLH73ac&^hw0I8z|R_MZI zHzP!!#{}-S_$w*PE&(92uS7og&Jz6OHg`Nk2GaH}4m{vX;l;W=-%ZiDS{c27$!GD%1g!K zaE&mKQr-osUIk?@LeM%;A(`jkFN1>-lN0%%Tgfy({bPgBsKA?Y3&4_OeQO$7w$ zX5nx)yjTv}y0Oe;qctpIVOzV%C0~v(p%!}1q}5VOvpH?wo~wVqyxWi#Yq6A>*4a$l zN6j2CUjAqMPz^tNU0=uvzRA($=5QR-j~@^^OhUT5Eh9Zp**@|vX`IC|^=B4+Ixn9t z6MQ~DrV2zQ(t(!HZn0R^RN@{P+CR@1)sj#x#j}w>Q>_M>mR|L>3x>oZ13!7t;MPKZ zB3`h;_i!gfvz`E&5A=rtkP1hPO5z6dGhB(u%9#Jqla_M6^CF!MtF$zPl2kq$7<{HF z3R+7Ew?08!%k(XqyGqs^U<~p?`qugZnqn#GL@_N5Y|59LxOW_)_@0_*dy-tgl$3V| z;SzoswQnl(UqBrDKJCL{>NYPt!D*!G?4gF?mR>Gvz`Jb}mUZ&o%{Wj?(Od-TJ`ymI zt^x~DT>bX;jpM@I0;$%aGCmfG^w~~OO*>pbDLuypZ&=>vP_6U&u+cATd%9H&)Q_(k zs{4c+_4+WylK)F2-Yl(xSx%xf1(^zE4KLb(wQW3y>01&P5DL2!Vw_>6jrv<3!p^Tk zcF@{Jz{8YU?P7Akqrl=R_(%4b+ERh;s#~38U0m7E++B(JgE|NS$er0!=pX<>-4?f_ zN$!W$NefhM^W$w6$azaTM(U|bXq*Z=$>ew}8&z+=QRhbzyu#-%pyTJzjD9<=KfJ2g zvHWMC;SD)w0`4F$E?u{-&wM}jk3|Ho2!n~{w^I3bXJCZ9a`FBS6k6=9%NK&LjlyI` zS}de~Hm&>zz=MdM6~s5zA*H@zAS_O*Xn32c>(L@2TR{y)%BYhDo%>N>J4Pl8qcaPb zL}cxe#_NHwW%CT^@b{H^Mdd&Lb+zgtkzp122OxOH`&LnB0&6quoXUkL6$8ngAbuo1 zSzZM~CY_s5EXIHbz1Rg9n__5uxkE#HcpXmVBlN9e2K{dc-j}O#$dDL|%gaC=y+hw| zxy202UXC~hOi{;UNjGuDG#{V=7Jla#Nzy+f*vVb+QEjzGJSfZ1j05*kzX_F?D+yK; zt-&X66!GCW0ABEAH&N;eZh}E2IFOyLbEuB7+YQqyOR`oN?^+m9)7=O-s1!1^_6VQ zcV1{Y9hM&PO9blfsm)*=1tU3p5hB;}eDKEt;4{&0Za-cC`^2l<;Ur1FjM?U3X`veU z&v?Bs*Fi+$+W0^(Y`t!i>iJh!+ZM zxpk6TKlc1c-r&Ax-yNmr(+>P*%_rrtX4Q(xQi%R=j5(E-<9H5~-qxJwHayk%Oc3;k zkpB4iZ6+!H!%UnVWA=k*<1w~Wpi`jcc_{-x_%9hXh=INQj^tF~`^%M_$;p6-e3=tn z$GlLQ)yEsln#RFUgIFW`)m&W1wJE2$Bu&1h{^B;@Z_5P3I$8_0PcB{-?Vgv?xpz&8 zF%6F6x{1VS3~HQ}+nF8z0}Ng?NiTK9*+D|j$}u+fT86kzYEJ8)&sUZ|Yxv5|>@nW? zsx&6>Lzh7l`XEE&#q^bh`uXQm3uVT+Um=sk?3OB&(c$YX;WCL^oGGXP%O9nYeT*ij zs`YgZzmm8dT{VoxCTa-3%wqNvL`uJFz=aKae(QMpQVI9u z5KEOne_-UFV0>b7B9x0=Ezo$CiP6}201y@sD8Yb5sO+g(nAA> z`BTa+2`WG6aD8&xJAdYRP30%GX1|z5M*j;V&s>Q4e&~S9gY!MM!)r2qogQ(QYlY>u z$th6X9~%DEb0ti$RI6_5yG4e)x1y&cfsUNV-)Ohru}8%0c24dl$J>2%9BR5T3x-d< z1v$1AO70fnJzPI{DqTaO?`<>U6|dwEU!b~zeK`D+8?$9Omn zPG@%%EYem#;E2-Z*!8z{pP6Hc;EjVaN|B@3X6uOqd5r@Rt+3e+3pR?J+in?r&g{h$5b! zf*+tIlxi91HUSEK|SH+A!lMqpWe8?n4xKI_{$lH>o|OD7IGoW9jq?D zP>eke93VYJPkNWVf5T7h9gD+3wWazI?t4}~?@iT= zx-mrrIL=3w7|_OC?}yICX#yC^^13S_8tHwp58vB7C6|!1M!U)0`p0dV6pAxAP_*a; zwLv4YRHJC_b+8S0?QdL!TnBag_T?*j`afpB*XyzV0rc;4l@Igd*q(pmK;$7f&^5)Nr^`F<=jYWYK+g?%-wat+AL~uWtcCDPCDPWnd~DBWujXSN~6yS z(@I+Ci2veGKA!zV)Yz>E0+TpE86^@?dCYGsGxsiNn5F4N@0n1>IV@mH1;;(66><^0 zFZ2On=et@&?9e_CRV7%eT!TiC>6vlJMp5B>S--|>mx78`PPQfgus|(;+5VUYW-=lGE)ePRLWNI3T)=Gb1NO9hL#z>RpTm6n{T8^rNr2 zG)JuuEABrE8%mR$LB#j$h8TX~ES}v=5ewj8K6P#Pnlu7{IR0XgF zMc}Hs5fhLsR4v!PPKp20*kz5OLIz~b7wF67_+i-T!CLRaA+J49lQ~d^{Uym9z|
MGDV3$B5T*`(mVhf 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 KlfKCRzW*Ql=`_Uv literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..652f92a04f1b1527961ce441b7e07fe02c8c0522 GIT binary patch literal 125206 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 z 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 literal 0 HcmV?d00001 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/models/__init__.py b/pms_api_rest/models/__init__.py index 4713a4d6b7..96f3e93d1d 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -2,3 +2,5 @@ from . import res_users from . import account_payment from . import sql_export +from . import pms_room_type_class + diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index cda0879373..c15c95dd1a 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -88,3 +88,8 @@ class PmsProperty(models.Model): 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, + ) 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/services/manage_url_images.py b/pms_api_rest/services/manage_url_images.py new file mode 100644 index 0000000000..2032e9b3e7 --- /dev/null +++ b/pms_api_rest/services/manage_url_images.py @@ -0,0 +1,19 @@ +from odoo import http + + +def url_image(context, model, record_id, field): + rt_image_attach = context.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 = ( + http.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/services/pms_agency_service.py b/pms_api_rest/services/pms_agency_service.py index d6895b5653..5866b623de 100644 --- a/pms_api_rest/services/pms_agency_service.py +++ b/pms_api_rest/services/pms_agency_service.py @@ -1,4 +1,5 @@ from odoo import _ +from odoo.addons.pms_api_rest.services.manage_url_images import url_image from odoo.exceptions import MissingError from odoo.addons.base_rest import restapi @@ -41,9 +42,7 @@ def get_agencies(self, agencies_search_param): PmsAgencyInfo( id=agency.id, name=agency.name, - image=agency.image_1024.decode("utf-8") - if agency.image_1024 - else None, + imageUrl=url_image(self, 'res.partner', agency.id, 'image_128'), ) ) return result_agencies @@ -72,7 +71,7 @@ def get_agency(self, agency_id): return PmsAgencieInfo( id=agency.id, name=agency.name if agency.name else None, - image=agency.image_1024.decode("utf-8") if agency.image_1024 else None, + imageUrl=url_image(self, 'res.partner', agency.id, 'image_128'), ) else: raise MissingError(_("Agency not found")) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 92c601cac2..f7cfb518c8 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -9,6 +9,7 @@ from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component +from .manage_url_images import url_image class PmsLoginService(Component): @@ -80,6 +81,7 @@ def login(self, user): userImageBase64=user_record.partner_id.image_1024 if user_record.partner_id.image_1024 else None, + userImageUrl=url_image(self, '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_property_service.py b/pms_api_rest/services/pms_property_service.py index c1acafe74a..38e7a61ec6 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -1,6 +1,7 @@ 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.services.manage_url_images import url_image class PmsPropertyService(Component): @@ -48,6 +49,7 @@ def get_properties(self): simpleInColor=prop.simple_in_color, simpleFutureColor=prop.simple_future_color, language=prop.lang, + hotelImageUrl=url_image(self, 'pms.property', prop.id, 'hotel_image_pms_api_rest'), ) ) return result_properties diff --git a/pms_api_rest/services/pms_room_type_class_service.py b/pms_api_rest/services/pms_room_type_class_service.py index 28dfa3ea0a..87f3e3d66c 100644 --- a/pms_api_rest/services/pms_room_type_class_service.py +++ b/pms_api_rest/services/pms_room_type_class_service.py @@ -1,6 +1,7 @@ from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component +from .manage_url_images import url_image class PmsRoomTypeClassService(Component): @@ -51,6 +52,7 @@ def get_room_type_class(self, room_type_class_search_param): name=room.name, defaultCode=room.default_code if room.default_code else None, pmsPropertyIds=room.pms_property_ids.mapped("id"), + imageUrl=url_image(self, 'pms.room.type.class', room.id, 'icon_pms_api_rest'), ) ) return result_room_type_class diff --git a/pms_api_rest/services/pms_sale_channel_service.py b/pms_api_rest/services/pms_sale_channel_service.py index 7c209912eb..1c84681572 100644 --- a/pms_api_rest/services/pms_sale_channel_service.py +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -1,4 +1,5 @@ from odoo import _ +from odoo.addons.pms_api_rest.services.manage_url_images import url_image from odoo.exceptions import MissingError from odoo.addons.base_rest import restapi @@ -62,9 +63,7 @@ def get_sale_channels(self, sale_channel_search_param): channelType=sale_channel.channel_type if sale_channel.channel_type else None, - icon=sale_channel.icon - if sale_channel.icon - else None, + iconUrl=url_image(self, 'pms.sale.channel', sale_channel.id, 'icon'), ) ) return result_sale_channels diff --git a/pms_api_rest/views/pms_property_views.xml b/pms_api_rest/views/pms_property_views.xml index 5c86deee6a..b47007b356 100644 --- a/pms_api_rest/views/pms_property_views.xml +++ b/pms_api_rest/views/pms_property_views.xml @@ -4,6 +4,11 @@ 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..3434234658 --- /dev/null +++ b/pms_api_rest/views/pms_room_type_class_views.xml @@ -0,0 +1,12 @@ + + + + pms.room.type.class + + + + + + + + From 6b2110c9a0b616b34b293bdd35076ff6543804c9 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 7 Sep 2023 12:11:36 +0200 Subject: [PATCH 449/547] [IMP] pms-api-rest: add authorImageUrl to folio % reservation messages service --- pms_api_rest/datamodels/pms_reservation_message.py | 2 ++ pms_api_rest/services/pms_folio_service.py | 9 ++++++--- pms_api_rest/services/pms_reservation_service.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/datamodels/pms_reservation_message.py b/pms_api_rest/datamodels/pms_reservation_message.py index 6ff581da26..3b004d7ed0 100644 --- a/pms_api_rest/datamodels/pms_reservation_message.py +++ b/pms_api_rest/datamodels/pms_reservation_message.py @@ -13,6 +13,7 @@ class PmsReservationMessageInfo(Datamodel): 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): @@ -23,6 +24,7 @@ class PmsFolioMessageInfo(Datamodel): 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): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 15d847ebd1..698d1a9515 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from odoo import _, fields +from .manage_url_images import url_image from odoo.exceptions import MissingError, ValidationError from odoo.osv import expression from odoo.tools import get_lang @@ -1257,6 +1258,7 @@ def get_folio_reservation_messages(self, folio_id): ).decode("utf-8") if message.author_id.image_1024 else None, + authorImageUrl=url_image(self, 'res.partner', message.author_id.id, 'image_1024'), ) ) PmsFolioMessageInfo = self.env.datamodels["pms.folio.message.info"] @@ -1268,9 +1270,9 @@ def get_folio_reservation_messages(self, folio_id): subject = folio_message.subject if folio_message.subject else None folio_messages.append( PmsFolioMessageInfo( - author=message.author_id.name - if message.author_id - else message.email_from, + 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"), @@ -1280,6 +1282,7 @@ def get_folio_reservation_messages(self, folio_id): ).decode("utf-8") if folio_message.author_id.image_1024 else None, + authorImageUrl=url_image(self, 'res.partner', folio_message.author_id.id, 'image_1024'), ) ) PmsMessageInfo = self.env.datamodels["pms.message.info"] diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index ccc767817d..50e44e59e9 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -271,7 +271,7 @@ def update_reservation(self, reservation_id, reservation_data): reservation.with_context(skip_compute_service_ids=True).write(reservation_vals) else: reservation.write(reservation_vals) - print(reservation.service_ids.mapped("name")) + # print(reservation.service_ids.mapped("name")) def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): From bfc1da0c3837954e90c3ae3eb021f92543e2f32e Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 8 Sep 2023 11:33:53 +0200 Subject: [PATCH 450/547] [REF] pms-api-rest: pms_api_rest_utils manage images from field to url --- .../manage_url_images.py => pms_api_rest_utils.py} | 8 ++++---- pms_api_rest/services/pms_agency_service.py | 7 ++++--- pms_api_rest/services/pms_folio_service.py | 7 ++++--- pms_api_rest/services/pms_login_service.py | 5 ++--- pms_api_rest/services/pms_property_service.py | 4 ++-- pms_api_rest/services/pms_room_type_class_service.py | 5 ++--- pms_api_rest/services/pms_sale_channel_service.py | 5 ++--- 7 files changed, 20 insertions(+), 21 deletions(-) rename pms_api_rest/{services/manage_url_images.py => pms_api_rest_utils.py} (71%) diff --git a/pms_api_rest/services/manage_url_images.py b/pms_api_rest/pms_api_rest_utils.py similarity index 71% rename from pms_api_rest/services/manage_url_images.py rename to pms_api_rest/pms_api_rest_utils.py index 2032e9b3e7..201d93a170 100644 --- a/pms_api_rest/services/manage_url_images.py +++ b/pms_api_rest/pms_api_rest_utils.py @@ -1,8 +1,8 @@ -from odoo import http +from odoo.http import request -def url_image(context, model, record_id, field): - rt_image_attach = context.env['ir.attachment'].sudo().search([ +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), @@ -10,7 +10,7 @@ def url_image(context, model, record_id, field): if rt_image_attach and not rt_image_attach.access_token: rt_image_attach.generate_access_token() result = ( - http.request.env['ir.config_parameter'] + 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 diff --git a/pms_api_rest/services/pms_agency_service.py b/pms_api_rest/services/pms_agency_service.py index 5866b623de..2fecc4709e 100644 --- a/pms_api_rest/services/pms_agency_service.py +++ b/pms_api_rest/services/pms_agency_service.py @@ -1,11 +1,12 @@ from odoo import _ -from odoo.addons.pms_api_rest.services.manage_url_images import url_image 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" @@ -42,7 +43,7 @@ def get_agencies(self, agencies_search_param): PmsAgencyInfo( id=agency.id, name=agency.name, - imageUrl=url_image(self, 'res.partner', agency.id, 'image_128'), + imageUrl=url_image_pms_api_rest('res.partner', agency.id, 'image_128'), ) ) return result_agencies @@ -71,7 +72,7 @@ def get_agency(self, agency_id): return PmsAgencieInfo( id=agency.id, name=agency.name if agency.name else None, - imageUrl=url_image(self, 'res.partner', agency.id, 'image_128'), + 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_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 698d1a9515..b817930f9d 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta from odoo import _, fields -from .manage_url_images import url_image from odoo.exceptions import MissingError, ValidationError from odoo.osv import expression from odoo.tools import get_lang @@ -12,6 +11,8 @@ 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__) @@ -1258,7 +1259,7 @@ def get_folio_reservation_messages(self, folio_id): ).decode("utf-8") if message.author_id.image_1024 else None, - authorImageUrl=url_image(self, 'res.partner', message.author_id.id, 'image_1024'), + authorImageUrl=url_image_pms_api_rest('res.partner', message.author_id.id, 'image_1024'), ) ) PmsFolioMessageInfo = self.env.datamodels["pms.folio.message.info"] @@ -1282,7 +1283,7 @@ def get_folio_reservation_messages(self, folio_id): ).decode("utf-8") if folio_message.author_id.image_1024 else None, - authorImageUrl=url_image(self, 'res.partner', folio_message.author_id.id, 'image_1024'), + authorImageUrl=url_image_pms_api_rest('res.partner', folio_message.author_id.id, 'image_1024'), ) ) PmsMessageInfo = self.env.datamodels["pms.message.info"] diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index f7cfb518c8..e6b7c0cfcb 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -9,8 +9,7 @@ from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component -from .manage_url_images import url_image - +from ..pms_api_rest_utils import url_image_pms_api_rest class PmsLoginService(Component): _inherit = "base.rest.service" @@ -81,7 +80,7 @@ def login(self, user): userImageBase64=user_record.partner_id.image_1024 if user_record.partner_id.image_1024 else None, - userImageUrl=url_image(self, 'res.partner', user_record.partner_id.id, 'image_1024'), + 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_property_service.py b/pms_api_rest/services/pms_property_service.py index 38e7a61ec6..df9c52731f 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -1,7 +1,7 @@ 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.services.manage_url_images import url_image +from ..pms_api_rest_utils import url_image_pms_api_rest class PmsPropertyService(Component): @@ -49,7 +49,7 @@ def get_properties(self): simpleInColor=prop.simple_in_color, simpleFutureColor=prop.simple_future_color, language=prop.lang, - hotelImageUrl=url_image(self, 'pms.property', prop.id, 'hotel_image_pms_api_rest'), + hotelImageUrl=url_image_pms_api_rest('pms.property', prop.id, 'hotel_image_pms_api_rest'), ) ) return result_properties diff --git a/pms_api_rest/services/pms_room_type_class_service.py b/pms_api_rest/services/pms_room_type_class_service.py index 87f3e3d66c..2d149623af 100644 --- a/pms_api_rest/services/pms_room_type_class_service.py +++ b/pms_api_rest/services/pms_room_type_class_service.py @@ -1,8 +1,7 @@ from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component -from .manage_url_images import url_image - +from ..pms_api_rest_utils import url_image_pms_api_rest class PmsRoomTypeClassService(Component): _inherit = "base.rest.service" @@ -52,7 +51,7 @@ def get_room_type_class(self, room_type_class_search_param): name=room.name, defaultCode=room.default_code if room.default_code else None, pmsPropertyIds=room.pms_property_ids.mapped("id"), - imageUrl=url_image(self, 'pms.room.type.class', room.id, 'icon_pms_api_rest'), + 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_sale_channel_service.py b/pms_api_rest/services/pms_sale_channel_service.py index 1c84681572..70fb514ea5 100644 --- a/pms_api_rest/services/pms_sale_channel_service.py +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -1,11 +1,10 @@ from odoo import _ -from odoo.addons.pms_api_rest.services.manage_url_images import url_image 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" @@ -63,7 +62,7 @@ def get_sale_channels(self, sale_channel_search_param): channelType=sale_channel.channel_type if sale_channel.channel_type else None, - iconUrl=url_image(self, 'pms.sale.channel', sale_channel.id, 'icon'), + iconUrl=url_image_pms_api_rest('pms.sale.channel', sale_channel.id, 'icon'), ) ) return result_sale_channels From 9ca8f59348ecd97a5fd3e1f7f93e08884e9f471f Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 9 Oct 2023 11:18:12 +0100 Subject: [PATCH 451/547] [FIX] pms-api-rest: fix issues @ endpoint routers for create massive changes and update (pricelist items & av. plan items) --- pms_api_rest/services/pms_availability_plan_service.py | 4 ++-- pms_api_rest/services/pms_pricelist_service.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index 2ef5c3b963..a28c8b859c 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -230,7 +230,7 @@ def _create_or_update_avail_plan_rules(self, pms_avail_plan_rules_info): [ ( [ - "//availability-plan-rules", + "/p//availability-plan-rules", ], "PATCH", ) @@ -255,7 +255,7 @@ def create_availability_plan_rule(self, availability_plan_id, pms_avail_plan_rul [ "/batch-changes", ], - "PATCH", + "POST", ) ], input_param=Datamodel("pms.availability.plan.rules.info", is_list=False), diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index bcbb79319d..6f7965a271 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -199,7 +199,7 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): [ ( [ - "//pricelist-items", + "/p//pricelist-items", ], "PATCH", ) @@ -220,7 +220,7 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): [ "/batch-changes", ], - "PATCH", + "POST", ) ], input_param=Datamodel("pms.pricelist.items.info", is_list=False), From 963963f50c2e177612f75c29223b160acfb8438d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 16 Oct 2023 16:56:04 +0200 Subject: [PATCH 452/547] [ADD]pms_api_rest: cash flow by turns --- pms_api_rest/models/__init__.py | 2 +- pms_api_rest/models/account_bank_statement.py | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 pms_api_rest/models/account_bank_statement.py diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 96f3e93d1d..3a33b55647 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -3,4 +3,4 @@ from . import account_payment from . import sql_export from . import pms_room_type_class - +from . import account_bank_statement 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 From 4e62d5ca6ff71ba01037d68d0ffee8b7b5ae4400 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 26 Jul 2023 17:58:14 +0200 Subject: [PATCH 453/547] [ADD] pms_api_rest: add dashboard service --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_dashboard.py | 18 +++++++ pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/services/__init__.py | 1 + .../services/pms_dashboard_service.py | 51 +++++++++++++++++++ pms_api_rest/services/pms_folio_service.py | 6 ++- 6 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 pms_api_rest/datamodels/pms_dashboard.py create mode 100644 pms_api_rest/services/pms_dashboard_service.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index dc54f816ac..2108ac656f 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -60,3 +60,4 @@ from . import pms_notification from . import pms_reservation_message from . import pms_avail +from . import pms_dashboard diff --git a/pms_api_rest/datamodels/pms_dashboard.py b/pms_api_rest/datamodels/pms_dashboard.py new file mode 100644 index 0000000000..e11ffee673 --- /dev/null +++ b/pms_api_rest/datamodels/pms_dashboard.py @@ -0,0 +1,18 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsDashboardCheckinsSearchParam(Datamodel): + _name = "pms.dashboard.checkins.search.param" + dateTo = fields.String(required=False, allow_none=True) + dateFrom = fields.String(required=False, allow_none=True) + + +class PmsDashboardCheckins(Datamodel): + _name = "pms.dashboard.checkins" + id = fields.Integer(required=True, allow_none=False) + checkinPartnerState = fields.String(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + + diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 76389921fb..822b3158cb 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -12,6 +12,7 @@ class PmsFolioSearchParam(Datamodel): 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) class PmsFolioInfo(Datamodel): diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index cc14bf2003..4d43a7359d 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -42,3 +42,4 @@ from . import pms_notification_service from . import pms_avail_service from . import pms_user_service +from . import pms_dashboard_service 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..2368129751 --- /dev/null +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -0,0 +1,51 @@ +from odoo.addons.component.core import Component +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo import fields +from datetime import datetime + + + +class PmsDashboardServices(Component): + _inherit = "base.rest.service" + _name = "pms.dashboard.service" + _usage = "dashboard" + _collection = "pms.services" + + @restapi.method( + [ + ( + [ + "/checkins", + ], + "GET", + ) + ], + input_param=Datamodel("pms.dashboard.checkins.search.param"), + output_param=Datamodel("pms.dashboard.checkins", is_list=True), + auth="jwt_api_pms", + ) + def get_checkins(self, pms_checkins_search_param): + date_from = fields.Date.from_string(pms_checkins_search_param.dateFrom) + date_to = fields.Date.from_string(pms_checkins_search_param.dateTo) + + domain = [ + ("checkin", ">=", date_from), + ("checkin", "<=", date_to), + ("state", "in", ("confirm", "arrival_delayed")), + ("reservation_type", "!=", "out") + ] + reservations = self.env["pms.reservation"].search(domain) + PmsDashboardCheckins = self.env.datamodels["pms.dashboard.checkins"] + result_checkins = [] + for checkin_partner in reservations.checkin_partner_ids: + result_checkins.append( + PmsDashboardCheckins( + id=checkin_partner.id, + checkinPartnerState=checkin_partner.state, + date=datetime.combine( + checkin_partner.checkin, datetime.min.time() + ).isoformat(), + ) + ) + return result_checkins diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index b817930f9d..78adfeff82 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -99,6 +99,10 @@ 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)) + today = fields.Datetime.now() + today = today.replace(hour=0, minute=0, second=0, microsecond=0) + if folio_search_param.last: + domain_fields.append(("create_date", ">", today)) if folio_search_param.dateTo and folio_search_param.dateFrom: date_from = fields.Date.from_string(folio_search_param.dateFrom) @@ -182,11 +186,9 @@ def get_folios(self, folio_search_param): 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), ("reservation_type", "!=", "out")], From ed58201184859f7e5b78d99c48910d6b7c9950e7 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 1 Sep 2023 10:27:54 +0200 Subject: [PATCH 454/547] [IMP] pms_api_rest: get_reservations in pms_dashboard_service --- pms_api_rest/datamodels/pms_dashboard.py | 13 +++---- .../services/pms_dashboard_service.py | 35 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/pms_api_rest/datamodels/pms_dashboard.py b/pms_api_rest/datamodels/pms_dashboard.py index e11ffee673..2a744aed4b 100644 --- a/pms_api_rest/datamodels/pms_dashboard.py +++ b/pms_api_rest/datamodels/pms_dashboard.py @@ -3,16 +3,17 @@ from odoo.addons.datamodel.core import Datamodel -class PmsDashboardCheckinsSearchParam(Datamodel): - _name = "pms.dashboard.checkins.search.param" +class PmsDashboardPendingReservationsSearchParam(Datamodel): + _name = "pms.dashboard.pending.reservations.search.param" dateTo = fields.String(required=False, allow_none=True) dateFrom = fields.String(required=False, allow_none=True) -class PmsDashboardCheckins(Datamodel): - _name = "pms.dashboard.checkins" +class PmsDashboardPendingReservations(Datamodel): + _name = "pms.dashboard.pending.reservations" id = fields.Integer(required=True, allow_none=False) - checkinPartnerState = fields.String(required=False, allow_none=True) - date = fields.String(required=False, allow_none=True) + state = fields.String(required=False, allow_none=True) + reservationType = fields.String(required=False, allow_none=True) + checkin = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index 2368129751..aadf442c77 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -16,36 +16,37 @@ class PmsDashboardServices(Component): [ ( [ - "/checkins", + "/reservations", ], "GET", ) ], - input_param=Datamodel("pms.dashboard.checkins.search.param"), - output_param=Datamodel("pms.dashboard.checkins", is_list=True), + input_param=Datamodel("pms.dashboard.pending.reservations.search.param"), + output_param=Datamodel("pms.dashboard.pending.reservations", is_list=True), auth="jwt_api_pms", ) - def get_checkins(self, pms_checkins_search_param): - date_from = fields.Date.from_string(pms_checkins_search_param.dateFrom) - date_to = fields.Date.from_string(pms_checkins_search_param.dateTo) + def get_reservations(self, pms_reservations_search_param): + date_from = fields.Date.from_string(pms_reservations_search_param.dateFrom) + date_to = fields.Date.from_string(pms_reservations_search_param.dateTo) domain = [ ("checkin", ">=", date_from), ("checkin", "<=", date_to), - ("state", "in", ("confirm", "arrival_delayed")), + ("state", "!=", "cancel"), ("reservation_type", "!=", "out") ] reservations = self.env["pms.reservation"].search(domain) - PmsDashboardCheckins = self.env.datamodels["pms.dashboard.checkins"] - result_checkins = [] - for checkin_partner in reservations.checkin_partner_ids: - result_checkins.append( - PmsDashboardCheckins( - id=checkin_partner.id, - checkinPartnerState=checkin_partner.state, - date=datetime.combine( - checkin_partner.checkin, datetime.min.time() + PmsDashboardPendingReservations = self.env.datamodels["pms.dashboard.pending.reservations"] + result = [] + for reservation in reservations: + result.append( + PmsDashboardPendingReservations( + id=reservation.id, + state=reservation.state, + checkin=datetime.combine( + reservation.checkin, datetime.min.time() ).isoformat(), + reservationType=reservation.reservation_type, ) ) - return result_checkins + return result From dc598f62854505dddc201a26f5c8e8305afdc6e8 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 5 Sep 2023 12:46:29 +0200 Subject: [PATCH 455/547] [IMP] pms_api_rest: get_pending_checkin_reservations & get_pending_checkout_reservations in pms_dashboard_service --- pms_api_rest/datamodels/pms_dashboard.py | 10 +-- .../services/pms_dashboard_service.py | 87 +++++++++++++------ 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/pms_api_rest/datamodels/pms_dashboard.py b/pms_api_rest/datamodels/pms_dashboard.py index 2a744aed4b..6b24b4bc89 100644 --- a/pms_api_rest/datamodels/pms_dashboard.py +++ b/pms_api_rest/datamodels/pms_dashboard.py @@ -5,15 +5,13 @@ class PmsDashboardPendingReservationsSearchParam(Datamodel): _name = "pms.dashboard.pending.reservations.search.param" - dateTo = fields.String(required=False, allow_none=True) - dateFrom = fields.String(required=False, allow_none=True) + date = fields.String(required=False, allow_none=True) + pmsPropertyId = fields.Integer(required=False, allow_none=True) class PmsDashboardPendingReservations(Datamodel): _name = "pms.dashboard.pending.reservations" - id = fields.Integer(required=True, allow_none=False) - state = fields.String(required=False, allow_none=True) - reservationType = fields.String(required=False, allow_none=True) - checkin = fields.String(required=False, allow_none=True) + pendingReservations = fields.Integer(required=False, allow_none=True) + completedReservations = fields.Integer(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index aadf442c77..d0b4cead70 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -16,37 +16,74 @@ class PmsDashboardServices(Component): [ ( [ - "/reservations", + "/pending-checkins", ], "GET", ) ], input_param=Datamodel("pms.dashboard.pending.reservations.search.param"), - output_param=Datamodel("pms.dashboard.pending.reservations", is_list=True), + output_param=Datamodel("pms.dashboard.pending.reservations"), auth="jwt_api_pms", ) - def get_reservations(self, pms_reservations_search_param): - date_from = fields.Date.from_string(pms_reservations_search_param.dateFrom) - date_to = fields.Date.from_string(pms_reservations_search_param.dateTo) - - domain = [ - ("checkin", ">=", date_from), - ("checkin", "<=", date_to), - ("state", "!=", "cancel"), - ("reservation_type", "!=", "out") - ] - reservations = self.env["pms.reservation"].search(domain) + def get_pending_checkin_reservations(self, pms_reservations_search_param): + date = fields.Date.from_string(pms_reservations_search_param.date) + + + pendingReservations = self.env["pms.reservation"].search_count( + [ + ("checkin", "=", date), + ("state", "in", ("confirm", "arrival_delayed")), + ("reservation_type", "!=", "out") + ] + ) + completedReservations = self.env["pms.reservation"].search_count( + [ + ("checkin", "=", date), + ("state", "=", "onboard"), + ] + ) PmsDashboardPendingReservations = self.env.datamodels["pms.dashboard.pending.reservations"] - result = [] - for reservation in reservations: - result.append( - PmsDashboardPendingReservations( - id=reservation.id, - state=reservation.state, - checkin=datetime.combine( - reservation.checkin, datetime.min.time() - ).isoformat(), - reservationType=reservation.reservation_type, - ) + + return PmsDashboardPendingReservations( + pendingReservations=pendingReservations, + completedReservations=completedReservations, + ) + + @restapi.method( + [ + ( + [ + "/pending-checkouts", + ], + "GET", ) - return result + ], + input_param=Datamodel("pms.dashboard.pending.reservations.search.param"), + output_param=Datamodel("pms.dashboard.pending.reservations"), + auth="jwt_api_pms", + ) + def get_pending_checkout_reservations(self, pms_reservations_search_param): + date = fields.Date.from_string(pms_reservations_search_param.date) + + + pending_reservations = self.env["pms.reservation"].search_count( + [ + ("checkout", "=", date), + ("state", "in", ("onboard", "departure_delayed")), + ("reservation_type", "!=", "out"), + ] + ) + completed_reservations = self.env["pms.reservation"].search_count( + [ + ("checkout", "=", date), + ("state", "=", "done"), + ] + ) + PmsDashboardPendingReservations = self.env.datamodels["pms.dashboard.pending.reservations"] + + return PmsDashboardPendingReservations( + pendingReservations=pending_reservations, + completedReservations=completed_reservations, + ) + + From f5b7f5918ba3bd62b3e391be2919b6ec94f0febc Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 6 Sep 2023 17:31:47 +0200 Subject: [PATCH 456/547] [IMP] pms_api_rest: improve dashboard_service with sql statements --- pms_api_rest/datamodels/pms_dashboard.py | 23 +- .../services/pms_dashboard_service.py | 217 ++++++++++++++---- 2 files changed, 194 insertions(+), 46 deletions(-) diff --git a/pms_api_rest/datamodels/pms_dashboard.py b/pms_api_rest/datamodels/pms_dashboard.py index 6b24b4bc89..fa6bf8cc99 100644 --- a/pms_api_rest/datamodels/pms_dashboard.py +++ b/pms_api_rest/datamodels/pms_dashboard.py @@ -3,15 +3,30 @@ from odoo.addons.datamodel.core import Datamodel -class PmsDashboardPendingReservationsSearchParam(Datamodel): - _name = "pms.dashboard.pending.reservations.search.param" +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" - pendingReservations = fields.Integer(required=False, allow_none=True) - completedReservations = fields.Integer(required=False, allow_none=True) + 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 PmsDashboardNumericResponse(Datamodel): + _name = "pms.dashboard.numeric.response" + value = fields.Float(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index d0b4cead70..96bc1fd5cb 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -16,74 +16,207 @@ class PmsDashboardServices(Component): [ ( [ - "/pending-checkins", + "/pending-reservations", ], "GET", ) ], - input_param=Datamodel("pms.dashboard.pending.reservations.search.param"), - output_param=Datamodel("pms.dashboard.pending.reservations"), + 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_checkin_reservations(self, pms_reservations_search_param): - date = fields.Date.from_string(pms_reservations_search_param.date) + 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) - - pendingReservations = self.env["pms.reservation"].search_count( - [ - ("checkin", "=", date), - ("state", "in", ("confirm", "arrival_delayed")), - ("reservation_type", "!=", "out") - ] + self.env.cr.execute( + f""" + 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 '2023-08-30' - CURRENT_DATE, date '2023-09-30' - CURRENT_DATE) date) d + LEFT JOIN pms_reservation r + ON (r.checkin = d.date OR r.checkout = d.date) + AND r.pms_property_id = 1 + AND r.reservation_type != 'out' + GROUP BY d.date + ORDER BY d.date; + """, + ( + dateFrom, + dateTo, + pms_dashboard_search_param.pmsPropertyId, + ), ) - completedReservations = self.env["pms.reservation"].search_count( - [ - ("checkin", "=", date), - ("state", "=", "onboard"), - ] + + + 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( + f""" + SELECT CEIL(l.num * 100.00 / tr.num_total_rooms) + 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 + """, + ( + date_occupancy, + pms_dashboard_search_param.pmsPropertyId, + pms_dashboard_search_param.pmsPropertyId, + ), ) - PmsDashboardPendingReservations = self.env.datamodels["pms.dashboard.pending.reservations"] - return PmsDashboardPendingReservations( - pendingReservations=pendingReservations, - completedReservations=completedReservations, + result = self.env.cr.dictfetchall() + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + + return DashboardNumericResponse( + value=result[0]["occupancy"] if result else 0, ) @restapi.method( [ ( [ - "/pending-checkouts", + "/billing", ], "GET", ) ], - input_param=Datamodel("pms.dashboard.pending.reservations.search.param"), - output_param=Datamodel("pms.dashboard.pending.reservations"), + input_param=Datamodel("pms.dashboard.range.dates.search.param"), + output_param=Datamodel("pms.dashboard.numeric.response"), auth="jwt_api_pms", ) - def get_pending_checkout_reservations(self, pms_reservations_search_param): - date = fields.Date.from_string(pms_reservations_search_param.date) + def get_billing(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) - - pending_reservations = self.env["pms.reservation"].search_count( - [ - ("checkout", "=", date), - ("state", "in", ("onboard", "departure_delayed")), - ("reservation_type", "!=", "out"), - ] + self.env.cr.execute( + f""" + 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 BETWEEN %s AND %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_from, + date_to, + pms_dashboard_search_param.pmsPropertyId, + ), ) - completed_reservations = self.env["pms.reservation"].search_count( - [ - ("checkout", "=", date), - ("state", "=", "done"), - ] + + result = self.env.cr.dictfetchall() + DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] + + return DashboardNumericResponse( + value=result[0]["billing"] if result else 0, ) - PmsDashboardPendingReservations = self.env.datamodels["pms.dashboard.pending.reservations"] - return PmsDashboardPendingReservations( - pendingReservations=pending_reservations, - completedReservations=completed_reservations, + + @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, + ) From 57220bb0d2d9afddf3bfaebf7580d1f519088e48 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Thu, 7 Sep 2023 10:45:27 +0200 Subject: [PATCH 457/547] [FIX] pms_api_rest: fix parameters in occupandy sql statement and name it --- pms_api_rest/services/pms_dashboard_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index 96bc1fd5cb..82d9bb091c 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -93,7 +93,7 @@ def get_occupancy(self, pms_dashboard_search_param): self.env.cr.execute( f""" - SELECT CEIL(l.num * 100.00 / tr.num_total_rooms) + SELECT CEIL(l.num * 100.00 / tr.num_total_rooms) AS occupancy FROM ( SELECT COUNT(1) num_total_rooms @@ -112,8 +112,8 @@ def get_occupancy(self, pms_dashboard_search_param): ) l """, ( - date_occupancy, pms_dashboard_search_param.pmsPropertyId, + date_occupancy, pms_dashboard_search_param.pmsPropertyId, ), ) From 4c9d28d09d26339175324941c1cdedbd9f1ce781 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 11 Sep 2023 11:05:19 +0200 Subject: [PATCH 458/547] [IMP] pms-api-rest: add userFirstName to login service --- pms_api_rest/datamodels/pms_user.py | 1 + pms_api_rest/datamodels/res_users.py | 1 + pms_api_rest/services/pms_login_service.py | 1 + 3 files changed, 3 insertions(+) diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index 63f9289927..07e52ac4f3 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -20,6 +20,7 @@ class PmsApiRestUserOutput(Datamodel): 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) diff --git a/pms_api_rest/datamodels/res_users.py b/pms_api_rest/datamodels/res_users.py index f003f46eb4..bd4bb4da1b 100644 --- a/pms_api_rest/datamodels/res_users.py +++ b/pms_api_rest/datamodels/res_users.py @@ -7,4 +7,5 @@ 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/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index e6b7c0cfcb..aa0333b805 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -73,6 +73,7 @@ def login(self, user): expirationDate=timestamp_expire_in_a_min, userId=user_record.id, userName=user_record.name, + userFirstName=user_record.firstname, userEmail=user_record.email if user_record.email else None, userPhone=user_record.phone if user_record.phone else None, defaultPropertyId=user_record.pms_property_id.id, From 0365b09abba516ecc7d88184c58182c0a6e2526b Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 20 Sep 2023 17:30:59 +0200 Subject: [PATCH 459/547] [IMP] pms_api_rest: change input_param to receive only one date and add services for operatives in dashboard_service, add createHour field in folio datamodel --- pms_api_rest/datamodels/pms_folio.py | 1 + .../services/pms_dashboard_service.py | 180 ++++++++++++++++-- pms_api_rest/services/pms_folio_service.py | 1 + pms_api_rest/services/pms_user_service.py | 1 + 4 files changed, 172 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 822b3158cb..c9d4dcedd3 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -74,3 +74,4 @@ class PmsFolioShortInfo(Datamodel): saleChannelId = fields.Integer(required=False, allow_none=True) firstCheckin = fields.String(required=False, allow_none=True) lastCheckout = fields.String(required=False, allow_none=True) + createHour = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index 82d9bb091c..016f86e41b 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -42,10 +42,10 @@ def get_pending_reservations(self, pms_dashboard_search_param): 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 '2023-08-30' - CURRENT_DATE, date '2023-09-30' - CURRENT_DATE) date) d + 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 = 1 + AND r.pms_property_id = %s AND r.reservation_type != 'out' GROUP BY d.date ORDER BY d.date; @@ -122,7 +122,7 @@ def get_occupancy(self, pms_dashboard_search_param): DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] return DashboardNumericResponse( - value=result[0]["occupancy"] if result else 0, + value=result[0]["occupancy"] if result[0]["occupancy"] else 0, ) @restapi.method( @@ -134,36 +134,34 @@ def get_occupancy(self, pms_dashboard_search_param): "GET", ) ], - input_param=Datamodel("pms.dashboard.range.dates.search.param"), + 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_from = fields.Date.from_string(pms_dashboard_search_param.dateFrom) - date_to = fields.Date.from_string(pms_dashboard_search_param.dateTo) + date_billing = fields.Date.from_string(pms_dashboard_search_param.date) self.env.cr.execute( f""" 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 BETWEEN %s AND %s + 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_from, - date_to, + date_billing, pms_dashboard_search_param.pmsPropertyId, ), ) result = self.env.cr.dictfetchall() DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] - + print(result) return DashboardNumericResponse( - value=result[0]["billing"] if result else 0, + value=result[0]["billing"] if result[0]['billing'] else 0, ) @@ -220,3 +218,163 @@ def get_revpar(self, pms_dashboard_search_param): 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( + f""" + SELECT COUNT(1) new_folios + FROM pms_folio f + WHERE 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( + f""" + 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( + f""" + 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( + f""" + 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, + ) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 78adfeff82..dacb6ae300 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -272,6 +272,7 @@ def get_folios(self, folio_search_param): else None, firstCheckin=str(folio.first_checkin), lastCheckout=str(folio.last_checkout), + createHour=folio.create_date.strftime("%H:%M"), ) ) return result_folios diff --git a/pms_api_rest/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index cf2845429b..654318b5ad 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -41,6 +41,7 @@ def get_user(self, user_id): return PmsUserInfo( userId=user.id, userName=user.name, + userFirstNmame=user.first_name if user.first_name else "", userEmail=user.email if user.email else "", userPhone=user.phone if user.phone else "", userImageBase64=user.image_1920 if user.image_1920 else "", From 6c8b2768825d94a89c4c800aa3d2d108b51a56b4 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 27 Sep 2023 11:51:11 +0000 Subject: [PATCH 460/547] [IMP] pms-api-rest: service dashboard state rooms (free/out/oc) --- pms_api_rest/datamodels/pms_dashboard.py | 7 +- .../services/pms_dashboard_service.py | 77 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_dashboard.py b/pms_api_rest/datamodels/pms_dashboard.py index fa6bf8cc99..21f880cd32 100644 --- a/pms_api_rest/datamodels/pms_dashboard.py +++ b/pms_api_rest/datamodels/pms_dashboard.py @@ -16,7 +16,6 @@ class PmsDashboardRangeDatesSearchParam(Datamodel): pmsPropertyId = fields.Integer(required=False, allow_none=True) - class PmsDashboardPendingReservations(Datamodel): _name = "pms.dashboard.pending.reservations" date = fields.String(required=False, allow_none=True) @@ -26,6 +25,12 @@ class PmsDashboardPendingReservations(Datamodel): 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 PmsDashboardNumericResponse(Datamodel): _name = "pms.dashboard.numeric.response" diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index 016f86e41b..4bf745e7ca 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -125,6 +125,83 @@ def get_occupancy(self, pms_dashboard_search_param): value=result[0]["occupancy"] if result[0]["occupancy"] else 0, ) + @restapi.method([ + ( + [ + "/rooms-state", + ], + "GET", + ) + ], + + 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( + f""" + SELECT d.date, + COALESCE(rln.num_occupied_rooms, 0) AS num_occupied_rooms, + COALESCE( rlo.num_out_of_service_rooms, 0) AS num_out_of_service_rooms, + COUNT(r.id) 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, + pms_room r + WHERE r.pms_property_id = %s + AND r.id NOT IN (SELECT room_id + FROM pms_reservation_line l + WHERE l.date = d.date + AND l.occupies_availability + AND l.pms_property_id = %s + ) + GROUP BY d.date, num_occupied_rooms, num_out_of_service_rooms + ORDER BY d.date + """, + ( + dateFrom, + dateTo, + pms_dashboard_search_param.pmsPropertyId, + 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( [ ( From 002a999000b9241e136bf3f9426453242506ccdf Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 28 Sep 2023 11:10:18 +0000 Subject: [PATCH 461/547] [FIX] pms-api-rest: change endpoint state-rooms & fix naming firstname user --- pms_api_rest/services/pms_dashboard_service.py | 2 +- pms_api_rest/services/pms_user_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index 4bf745e7ca..3b462c9793 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -128,7 +128,7 @@ def get_occupancy(self, pms_dashboard_search_param): @restapi.method([ ( [ - "/rooms-state", + "/state-rooms", ], "GET", ) diff --git a/pms_api_rest/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index 654318b5ad..9eb67efcde 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -41,7 +41,7 @@ def get_user(self, user_id): return PmsUserInfo( userId=user.id, userName=user.name, - userFirstNmame=user.first_name if user.first_name else "", + 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 "", From 38ea9c87377b45b9a2311ce461aac25e2f4b0c27 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 9 Oct 2023 09:10:10 +0100 Subject: [PATCH 462/547] [IMP] pms-api-rest: dashboard services for graphs --- pms_api_rest/datamodels/pms_dashboard.py | 13 ++ .../services/pms_dashboard_service.py | 169 +++++++++++++++++- 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/datamodels/pms_dashboard.py b/pms_api_rest/datamodels/pms_dashboard.py index 21f880cd32..454de704b6 100644 --- a/pms_api_rest/datamodels/pms_dashboard.py +++ b/pms_api_rest/datamodels/pms_dashboard.py @@ -32,6 +32,19 @@ class PmsDashboardStateRooms(Datamodel): 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/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index 3b462c9793..34035edd4e 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -202,6 +202,65 @@ def get_state_rooms(self, pms_dashboard_search_param): ) 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), + ) + 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( + f""" + 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( [ ( @@ -236,12 +295,10 @@ def get_billing(self, pms_dashboard_search_param): result = self.env.cr.dictfetchall() DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] - print(result) return DashboardNumericResponse( value=result[0]["billing"] if result[0]['billing'] else 0, ) - @restapi.method( [ ( @@ -417,7 +474,6 @@ def get_cancelled_overnights(self, pms_dashboard_search_param): value=result[0]["cancelled_overnights"] if result[0]["cancelled_overnights"] else 0, ) - @restapi.method( [ ( @@ -455,3 +511,110 @@ def get_overbookings(self, pms_dashboard_search_param): 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), + ) + 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( + f""" + 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), + ) + 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( + f""" + 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 From c50824987309961d3f1a409aa827679a88cc0a58 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 17 Oct 2023 11:14:33 +0200 Subject: [PATCH 463/547] [ADD] pms_api_rest: add rss_post service, datamodel and add feed_rss module in pms_api_rest depends --- pms_api_rest/__manifest__.py | 1 + pms_api_rest/datamodels/__init__.py | 2 ++ pms_api_rest/datamodels/feed_post.py | 15 +++++++++ pms_api_rest/services/__init__.py | 1 + pms_api_rest/services/feed_post_service.py | 39 ++++++++++++++++++++++ 5 files changed, 58 insertions(+) create mode 100644 pms_api_rest/datamodels/feed_post.py create mode 100644 pms_api_rest/services/feed_post_service.py diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 1f338e16c8..a6d0469a46 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -15,6 +15,7 @@ "base_location", "l10n_es_aeat", "sql_export_excel", + "feed_rss", ], "external_dependencies": { "python": ["jwt", "simplejson", "marshmallow", "jose"], diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 2108ac656f..7c4f399dbb 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -61,3 +61,5 @@ from . import pms_reservation_message from . import pms_avail from . import pms_dashboard +from . import feed_post + diff --git a/pms_api_rest/datamodels/feed_post.py b/pms_api_rest/datamodels/feed_post.py new file mode 100644 index 0000000000..a44e02a0d0 --- /dev/null +++ b/pms_api_rest/datamodels/feed_post.py @@ -0,0 +1,15 @@ +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/services/__init__.py b/pms_api_rest/services/__init__.py index 4d43a7359d..f280265d15 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -43,3 +43,4 @@ from . import pms_avail_service from . import pms_user_service from . import pms_dashboard_service +from . import feed_post_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..3fd605ca57 --- /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 if rss.image_url else "", + ) + ) + return result_rss From 3ddde0c0d98d1d0cabbda9d8fa9b027e0fda1f5b Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 20 Oct 2023 11:41:36 +0200 Subject: [PATCH 464/547] [FIX]pms_api_rest: message dates in users timezone instead of UTC --- pms_api_rest/services/pms_folio_service.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index dacb6ae300..addd502075 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -10,6 +10,7 @@ from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component +import pytz from ..pms_api_rest_utils import url_image_pms_api_rest @@ -1237,11 +1238,14 @@ def get_folio_reservation_messages(self, folio_id): 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 @@ -1255,7 +1259,7 @@ def get_folio_reservation_messages(self, folio_id): else message.email_from, message=message_body, subject=subject, - date=message.date.strftime("%d/%m/%y %H:%M:%S"), + date=reservation_message_date.strftime("%d/%m/%y %H:%M:%S"), messageType=message.message_type, authorImageBase64=base64.b64encode( message.author_id.image_1024 @@ -1272,6 +1276,8 @@ def get_folio_reservation_messages(self, folio_id): 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 @@ -1279,7 +1285,7 @@ def get_folio_reservation_messages(self, folio_id): else folio_message.email_from, message=message_body, subject=subject, - date=folio_message.date.strftime("%d/%m/%y %H:%M:%S"), + 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 From 946cb014d3cdb9d4be0e82caac860710a705bef9 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 18 Oct 2023 11:01:38 +0200 Subject: [PATCH 465/547] =?UTF-8?q?[IMP]=20pms=5Fapi=5Frest:=20fix=20login?= =?UTF-8?q?=20service=20when=20user=20hasn=C2=B4t=20firstname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pms_api_rest/services/pms_login_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index aa0333b805..744e3fc1d3 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -73,9 +73,9 @@ def login(self, user): expirationDate=timestamp_expire_in_a_min, userId=user_record.id, userName=user_record.name, - userFirstName=user_record.firstname, - userEmail=user_record.email if user_record.email else None, - userPhone=user_record.phone if user_record.phone else None, + 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 From bc62416f4509723d62b9a57c4189bd8c6858181f Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 18 Oct 2023 11:03:04 +0200 Subject: [PATCH 466/547] =?UTF-8?q?[IMP]=20pms=5Fapi=5Frest:=20fix=20last?= =?UTF-8?q?=20folios=20service=20when=20there=20aren=C2=B4t=20reservations?= =?UTF-8?q?=20today?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pms_api_rest/services/pms_folio_service.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index addd502075..f2ba455295 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -100,10 +100,9 @@ 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)) - today = fields.Datetime.now() - today = today.replace(hour=0, minute=0, second=0, microsecond=0) + order_field = "write_date desc" if folio_search_param.last: - domain_fields.append(("create_date", ">", today)) + 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) @@ -193,7 +192,7 @@ def get_folios(self, folio_search_param): PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] for folio in self.env["pms.folio"].search( [("id", "in", reservations_result), ("reservation_type", "!=", "out")], - order="write_date desc", + order=order_field, limit=folio_search_param.limit, offset=folio_search_param.offset, ): From 7bd5ab4778c2e3e1c76a73fa2ccdee0afb987480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 26 Oct 2023 10:00:24 +0200 Subject: [PATCH 467/547] [IMP]pms_api_rest: reset-password public auth PATCH by POST --- pms_api_rest/services/pms_user_service.py | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pms_api_rest/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index 9eb67efcde..9d550076a1 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -1,18 +1,16 @@ import base64 -import tempfile 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 odoo.exceptions import AccessDenied -from datetime import datetime, timedelta - - - -from odoo import _ -from odoo.exceptions import MissingError class PmsRoomTypeClassService(Component): @@ -33,7 +31,6 @@ class PmsRoomTypeClassService(Component): 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: @@ -96,7 +93,7 @@ def write_user(self, user_id, input_data): else: user.write( { - "image_1024": '', + "image_1024": "", } ) return True @@ -122,7 +119,6 @@ def change_password(self, user_id, input_data): 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"] @@ -136,7 +132,7 @@ def change_password(self, user_id, input_data): [ "/p/reset-password", ], - "PATCH", + "POST", ) ], input_param=Datamodel("pms.api.rest.user.input", is_list=False), @@ -150,7 +146,6 @@ def reset_password(self, input_data): self.env["res.users"].sudo().signup(values, input_data.resetToken) return True - @restapi.method( [ ( @@ -165,19 +160,22 @@ def reset_password(self, input_data): cors="*", ) def send_mail_to_reset_password(self, input_data): - user = self.env["res.users"].sudo().search([("email", "=", input_data.userEmail)]) + 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) + 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) + template.with_context({"app_url": input_data.url}).send_mail( + user.id, force_send=True + ) return True return False - @restapi.method( [ ( @@ -191,7 +189,9 @@ def send_mail_to_reset_password(self, input_data): cors="*", ) def check_reset_password_token(self, reset_token): - user = self.env["res.partner"].sudo().search([("signup_token", "=", reset_token)]) + user = ( + self.env["res.partner"].sudo().search([("signup_token", "=", reset_token)]) + ) is_token_expired = False if not user: return True From c1ae64ad8594f3939506aacc8db101ce0f852640 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 7 Nov 2023 16:52:41 +0000 Subject: [PATCH 468/547] [IMP] pms-api-rest: fix property, feeds, folio & dashboard services 4 dashboard --- pms_api_rest/datamodels/pms_folio.py | 1 + pms_api_rest/datamodels/pms_property.py | 1 + pms_api_rest/services/feed_post_service.py | 2 +- .../services/pms_dashboard_service.py | 100 +++++++++++++++--- pms_api_rest/services/pms_folio_service.py | 7 ++ pms_api_rest/services/pms_property_service.py | 4 + 6 files changed, 101 insertions(+), 14 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index c9d4dcedd3..80385bd94c 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -67,6 +67,7 @@ class PmsFolioShortInfo(Datamodel): 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) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index caf3b49022..f723b3c02c 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -12,6 +12,7 @@ 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) diff --git a/pms_api_rest/services/feed_post_service.py b/pms_api_rest/services/feed_post_service.py index 3fd605ca57..2703f86770 100644 --- a/pms_api_rest/services/feed_post_service.py +++ b/pms_api_rest/services/feed_post_service.py @@ -33,7 +33,7 @@ def get_feed_posts(self): description=rss.description, publishDate=str(rss.publish_date), author=rss.author if rss.author else "", - imageUrl=rss.image_url if rss.image_url else "", + imageUrl="https://www.roomdoo.com/wp-content/uploads/2021/09/hotel-roomdoo.png" ) ) return result_rss diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index 34035edd4e..2d16f4890e 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -74,7 +74,6 @@ def get_pending_reservations(self, pms_dashboard_search_param): ) return pending_reservations - @restapi.method( [ ( @@ -133,7 +132,7 @@ def get_occupancy(self, pms_dashboard_search_param): "GET", ) ], - + auth="jwt_api_pms", input_param=Datamodel("pms.dashboard.range.dates.search.param"), output_param=Datamodel("pms.dashboard.state.rooms", is_list=True), ) @@ -210,9 +209,9 @@ def get_state_rooms(self, pms_dashboard_search_param): "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) @@ -373,7 +372,7 @@ def get_number_of_new_folios(self, pms_dashboard_search_param): f""" SELECT COUNT(1) new_folios FROM pms_folio f - WHERE f.create_date = %s + WHERE DATE(f.create_date) = %s AND f.state != 'cancel' AND f.pms_property_id = %s AND f.reservation_type NOT IN ('out', 'staff') @@ -523,6 +522,7 @@ def get_overbookings(self, pms_dashboard_search_param): 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) @@ -565,18 +565,17 @@ def get_occupied_rooms(self, pms_dashboard_search_param): ) return occupied_rooms_result - @restapi.method([ - ( - [ - "/daily-billings", - ], - "GET", - ) - ], - + ( + [ + "/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) @@ -618,3 +617,78 @@ def get_daily_billings(self, pms_dashboard_search_param): ) ) 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), + ], + limit=pms_folio_search_param.limit, + offset=pms_folio_search_param.offset, + order="create_date desc", + ): + print(folio.id) + 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), + createHour=folio.create_date.strftime("%H:%M"), + ) + ) + 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), + ], + ) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index f2ba455295..fd0e70a670 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -132,6 +132,13 @@ def get_folios(self, folio_search_param): 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.filter: target = folio_search_param.filter if "@" in target: diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index df9c52731f..77986da2e6 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -29,10 +29,14 @@ def get_properties(self): for prop in self.env["pms.property"].search( domain, ): + state_name = False + if prop.state_id: + state_name = self.env['res.country.state'].search([('id', '=', prop.state_id.id)]).name result_properties.append( PmsPropertyInfo( id=prop.id, name=prop.name, + stateName=state_name if state_name else None, defaultPricelistId=prop.default_pricelist_id.id, colorOptionConfig=prop.color_option_config, preReservationColor=prop.pre_reservation_color, From 7ec0f8c25a17281c606bb141ad7cc21c9ce77df1 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 24 Oct 2023 14:45:49 +0200 Subject: [PATCH 469/547] [IMP]14.0-pms_api_rest: domain for transaction type filters --- .../services/pms_transaction_service.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index f88db5ab8b..4c11c13327 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -81,19 +81,23 @@ def get_transactions(self, pms_transactions_search_param): ) if pms_transactions_search_param.transactionType: - domain_fields.append( - ( - "pms_api_transaction_type", - "=", - 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) From f93853255bc5f7d99eba742ee54814a7bd9c7208 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 10 Oct 2023 18:09:33 +0200 Subject: [PATCH 470/547] [IMP]pms_api_rest: changes in partners, partner invoices and partner transactions get --- pms_api_rest/services/pms_partner_service.py | 112 ++++++++++++++----- 1 file changed, 86 insertions(+), 26 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index f6cdc651cf..e2026c9835 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -84,8 +84,6 @@ def get_partners(self, pms_partner_search_params): result_partners = [] domain = [] - if pms_partner_search_params.name: - domain.append(("name", "ilike", pms_partner_search_params.name)) if pms_partner_search_params.housed: partners_housed = ( self.env["pms.checkin.partner"] @@ -93,17 +91,17 @@ def get_partners(self, pms_partner_search_params): .mapped("partner_id") ) domain.append(("id", "in", partners_housed.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": + if pms_partner_search_params.filterByType: + filter_by_type = pms_partner_search_params.filterByType.split(",") + + if "individual" in filter_by_type: domain.append(("is_company", "=", False)) domain.append(("is_agency", "=", False)) + if "company" in filter_by_type: + domain.append(("is_company", "=", True)) + if "agency" in filter_by_type: + domain.append(("is_agency", "=", True)) + if pms_partner_search_params.filter: subdomains = [ [("vat", "ilike", pms_partner_search_params.filter)], @@ -128,7 +126,6 @@ def get_partners(self, pms_partner_search_params): for partner in self.env["res.partner"].search( domain, - order=pms_partner_search_params.orderBy, limit=pms_partner_search_params.limit, offset=pms_partner_search_params.offset, ): @@ -281,6 +278,7 @@ def get_partner_payments(self, 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 @@ -291,10 +289,11 @@ def get_partner_payments(self, partner_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 or 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, @@ -327,21 +326,82 @@ def get_partner_invoices(self, partner_id): ("move_type", "in", self.env["account.move"].get_invoice_types()), ] ) - PmsAcoountMoveInfo = self.env.datamodels["pms.invoice.info"] invoices = [] - for invoice in partnerInvoices: - invoices.append( - PmsAcoountMoveInfo( - id=invoice.id, - name=invoice.name, - amount=round(invoice.amount_total, 2), - date=invoice.date.strftime("%d/%m/%Y"), - state=invoice.state, - paymentState=invoice.payment_state - if invoice.payment_state - else None, + 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( From 7fe5ae232dcca01e2580eec8db52271d4f655c0a Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 13 Oct 2023 14:20:57 +0200 Subject: [PATCH 471/547] [IMP]pms_api_rest: added partners housed now, last week and last month in partners get --- pms_api_rest/datamodels/pms_partner.py | 4 +- pms_api_rest/services/pms_partner_service.py | 60 ++++++++++++++++---- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index a2252a7cec..7c6f33a414 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -11,7 +11,9 @@ class PmsPartnerSearchParam(Datamodel): documentType = fields.Integer(required=False, allow_none=True) documentNumber = fields.String(required=False, allow_none=True) name = fields.String(required=False, allow_none=True) - housed = fields.Boolean(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) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index e2026c9835..85fda3ed69 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -1,5 +1,5 @@ import re -from datetime import datetime +from datetime import datetime, date, timedelta from odoo.osv import expression @@ -83,24 +83,60 @@ class PmsPartnerService(Component): def get_partners(self, pms_partner_search_params): result_partners = [] domain = [] + print("pms_partner_search_params", pms_partner_search_params) - if pms_partner_search_params.housed: - partners_housed = ( + 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.ids)) - if pms_partner_search_params.filterByType: - filter_by_type = pms_partner_search_params.filterByType.split(",") - - if "individual" in filter_by_type: - domain.append(("is_company", "=", False)) - domain.append(("is_agency", "=", False)) - if "company" in filter_by_type: + 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)) - if "agency" in filter_by_type: + 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 = [ From bfb02e32f0d7c6a7e50002d10cdad52ad4230e50 Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 19 Dec 2023 17:00:31 +0100 Subject: [PATCH 472/547] [FIX]14.0-pms_api_rest: fix pre-commit --- pms_api_rest/__manifest__.py | 1 + .../data/pms_app_reset_password_template.xml | 92 ++++++-- pms_api_rest/datamodels/__init__.py | 1 - pms_api_rest/datamodels/feed_post.py | 3 +- pms_api_rest/datamodels/pms_dashboard.py | 6 +- pms_api_rest/datamodels/pms_invoice.py | 8 +- pms_api_rest/datamodels/pms_user.py | 1 + pms_api_rest/datamodels/res_city_zip.py | 2 + .../demo/pms_api_rest_master_data.xml | 78 ++++--- pms_api_rest/pms_api_rest_utils.py | 28 ++- pms_api_rest/services/feed_post_service.py | 5 +- pms_api_rest/services/pms_agency_service.py | 6 +- .../services/pms_availability_plan_service.py | 19 +- .../services/pms_board_service_service.py | 4 +- pms_api_rest/services/pms_calendar_service.py | 216 ++++++++++-------- .../services/pms_dashboard_service.py | 210 ++++++++++------- pms_api_rest/services/pms_folio_service.py | 21 +- .../services/pms_id_category_service.py | 6 +- pms_api_rest/services/pms_login_service.py | 6 +- .../services/pms_notification_service.py | 1 + pms_api_rest/services/pms_partner_service.py | 215 +++++++++-------- pms_api_rest/services/pms_price_service.py | 8 +- .../services/pms_pricelist_service.py | 14 +- pms_api_rest/services/pms_property_service.py | 11 +- .../services/pms_reservation_service.py | 43 ++-- pms_api_rest/services/pms_room_service.py | 9 +- .../services/pms_room_type_class_service.py | 6 +- .../services/pms_sale_channel_service.py | 6 +- .../services/pms_transaction_service.py | 14 +- pms_api_rest/services/res_city_zip_service.py | 6 +- pms_api_rest/services/res_country_service.py | 4 +- .../services/res_partner_category_service.py | 6 +- pms_api_rest/views/pms_property_views.xml | 11 +- .../views/pms_room_type_class_views.xml | 2 +- 34 files changed, 658 insertions(+), 411 deletions(-) diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index a6d0469a46..fdde38e461 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -27,6 +27,7 @@ "views/pms_property_views.xml", "views/res_users_views.xml", "views/pms_room_type_class_views.xml", + "views/product_template_views.xml", ], "demo": [ "demo/pms_api_rest_master_data.xml", diff --git a/pms_api_rest/data/pms_app_reset_password_template.xml b/pms_api_rest/data/pms_app_reset_password_template.xml index 6de5976775..82b340c662 100644 --- a/pms_api_rest/data/pms_app_reset_password_template.xml +++ b/pms_api_rest/data/pms_app_reset_password_template.xml @@ -1,20 +1,39 @@ - + - Pms Reset Password - + Restablecer Contraseña - "${object.company_id.name | safe}" <${(object.company_id.email or user.email) | safe}> + "${object.company_id.name | safe}" <${(object.company_id.email or user.email) | safe}> ${object.email_formatted | safe} -
- +
+
- +
@@ -41,22 +70,31 @@
@@ -27,13 +46,23 @@
- +
${object.name} - ${object.company_id.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.

+ If you do not expect this, you can safely ignore this email.

Thanks, % if user.signature: -
+
${user.signature | safe} % endif
@@ -67,17 +105,29 @@
- +
${object.company_id.name}
${object.company_id.phone} % if object.company_id.email - | ${object.company_id.email} + | ${object.company_id.email} % endif % if object.company_id.website - | + | ${object.company_id.website} % endif @@ -92,8 +142,6 @@
${object.lang} - - - - + + diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 7c4f399dbb..726e661970 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -62,4 +62,3 @@ from . import pms_avail from . import pms_dashboard from . import feed_post - diff --git a/pms_api_rest/datamodels/feed_post.py b/pms_api_rest/datamodels/feed_post.py index a44e02a0d0..b6e2861a7c 100644 --- a/pms_api_rest/datamodels/feed_post.py +++ b/pms_api_rest/datamodels/feed_post.py @@ -4,7 +4,7 @@ class FeedPost(Datamodel): - _name ="feed.post.info" + _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) @@ -12,4 +12,3 @@ class FeedPost(Datamodel): 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_dashboard.py b/pms_api_rest/datamodels/pms_dashboard.py index 454de704b6..bbc011cc24 100644 --- a/pms_api_rest/datamodels/pms_dashboard.py +++ b/pms_api_rest/datamodels/pms_dashboard.py @@ -36,7 +36,9 @@ class PmsDashboardStateRooms(Datamodel): 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) + percentageReservationsSoldBySaleChannel = fields.Integer( + required=False, allow_none=True + ) class PmsDashboardNumericResponse(Datamodel): @@ -45,6 +47,6 @@ class PmsDashboardNumericResponse(Datamodel): class PmsDashboardDailyBilling(Datamodel): - _name ="pms.dashboard.daily.billing" + _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_invoice.py b/pms_api_rest/datamodels/pms_invoice.py index d3ea667ef2..0c1506a2e2 100644 --- a/pms_api_rest/datamodels/pms_invoice.py +++ b/pms_api_rest/datamodels/pms_invoice.py @@ -3,6 +3,7 @@ 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" @@ -14,6 +15,7 @@ class PmsInvoiceSearchParam(Datamodel): 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) @@ -25,7 +27,9 @@ class PmsAccountInvoiceInfo(Datamodel): # 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) + 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) @@ -42,9 +46,9 @@ class PmsAccountInvoiceInfo(Datamodel): 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) - diff --git a/pms_api_rest/datamodels/pms_user.py b/pms_api_rest/datamodels/pms_user.py index 07e52ac4f3..e82789b43f 100644 --- a/pms_api_rest/datamodels/pms_user.py +++ b/pms_api_rest/datamodels/pms_user.py @@ -32,6 +32,7 @@ class PmsApiRestUserOutput(Datamodel): 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/res_city_zip.py b/pms_api_rest/datamodels/res_city_zip.py index d3569ae8c3..cc19513a1a 100644 --- a/pms_api_rest/datamodels/res_city_zip.py +++ b/pms_api_rest/datamodels/res_city_zip.py @@ -6,6 +6,8 @@ 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) diff --git a/pms_api_rest/demo/pms_api_rest_master_data.xml b/pms_api_rest/demo/pms_api_rest_master_data.xml index 555f68eb8b..f0bee25861 100644 --- a/pms_api_rest/demo/pms_api_rest_master_data.xml +++ b/pms_api_rest/demo/pms_api_rest_master_data.xml @@ -1,32 +1,50 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/pms_api_rest/pms_api_rest_utils.py b/pms_api_rest/pms_api_rest_utils.py index 201d93a170..6a13093f1e 100644 --- a/pms_api_rest/pms_api_rest_utils.py +++ b/pms_api_rest/pms_api_rest_utils.py @@ -2,18 +2,24 @@ 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), - ]) + 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 + 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 '' + return result if result else "" diff --git a/pms_api_rest/services/feed_post_service.py b/pms_api_rest/services/feed_post_service.py index 2703f86770..27d5660cb3 100644 --- a/pms_api_rest/services/feed_post_service.py +++ b/pms_api_rest/services/feed_post_service.py @@ -24,7 +24,7 @@ class PmsFeedRss(Component): 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'): + for rss in self.env["rss.post"].search([], limit=5, order="publish_date desc"): result_rss.append( PmsFeedRss( postId=rss.post_id, @@ -33,7 +33,8 @@ def get_feed_posts(self): description=rss.description, publishDate=str(rss.publish_date), author=rss.author if rss.author else "", - imageUrl="https://www.roomdoo.com/wp-content/uploads/2021/09/hotel-roomdoo.png" + imageUrl="https://www.roomdoo.com/wp-content" + "/uploads/2021/09/hotel-roomdoo.png", ) ) return result_rss diff --git a/pms_api_rest/services/pms_agency_service.py b/pms_api_rest/services/pms_agency_service.py index 2fecc4709e..f5e48a1975 100644 --- a/pms_api_rest/services/pms_agency_service.py +++ b/pms_api_rest/services/pms_agency_service.py @@ -43,7 +43,9 @@ def get_agencies(self, agencies_search_param): PmsAgencyInfo( id=agency.id, name=agency.name, - imageUrl=url_image_pms_api_rest('res.partner', agency.id, 'image_128'), + imageUrl=url_image_pms_api_rest( + "res.partner", agency.id, "image_128" + ), ) ) return result_agencies @@ -72,7 +74,7 @@ def get_agency(self, agency_id): 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'), + 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_availability_plan_service.py b/pms_api_rest/services/pms_availability_plan_service.py index a28c8b859c..17416f5114 100644 --- a/pms_api_rest/services/pms_availability_plan_service.py +++ b/pms_api_rest/services/pms_availability_plan_service.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta +from odoo import _ from odoo.exceptions import MissingError, ValidationError from odoo.addons.base_rest import restapi @@ -238,14 +239,24 @@ def _create_or_update_avail_plan_rules(self, pms_avail_plan_rules_info): 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): + 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 + 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") + 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) diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index 957628e167..9b087f4fe5 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -52,7 +52,9 @@ def get_board_services(self, board_services_search_param): roomTypeId=board_service.pms_room_type_id.id, amount=round(board_service.amount, 2), boardServiceId=board_service.pms_board_service_id, - productIds=board_service.board_service_line_ids.mapped("product_id.id"), + productIds=board_service.board_service_line_ids.mapped( + "product_id.id" + ), ) ) return result_board_services diff --git a/pms_api_rest/services/pms_calendar_service.py b/pms_api_rest/services/pms_calendar_service.py index fd755f1faa..5b31fa2cfb 100644 --- a/pms_api_rest/services/pms_calendar_service.py +++ b/pms_api_rest/services/pms_calendar_service.py @@ -5,7 +5,7 @@ from odoo.addons.component.core import Component -def build_reservation_line_info( calendar_item, previous_item=False, next_item=False): +def build_reservation_line_info(calendar_item, previous_item=False, next_item=False): next_itemSplitted = ( calendar_item["splitted"] and next_item @@ -25,29 +25,39 @@ def build_reservation_line_info( calendar_item, previous_item=False, next_item=F ) ) 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'], - "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, + "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"], + "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, } @@ -55,20 +65,20 @@ def build_reservation_line_info( calendar_item, previous_item=False, next_item=F 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']}) + 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 @@ -165,7 +175,7 @@ def get_old_reservations_calendar(self, calendar_search_param): item for item in lines if item["reservation_id"] == line["reservation_id"] - and item["date"] == line["date"] + timedelta(days=1) + and item["date"] == line["date"] + timedelta(days=1) ), False, ) @@ -177,7 +187,7 @@ def get_old_reservations_calendar(self, calendar_search_param): item for item in lines if item["reservation_id"] == line["reservation_id"] - and item["date"] == line["date"] + timedelta(days=-1) + and item["date"] == line["date"] + timedelta(days=-1) ), False, ) @@ -277,24 +287,25 @@ def get_reservations_calendar(self, calendar_search_param): 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 + 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 + (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 id, state, price_day_total, room_id, date, reservation_id - FROM pms_reservation_line - WHERE pms_property_id = %s AND state != 'cancel' - AND occupies_availability = true AND date <= %s + LEFT OUTER JOIN ( + SELECT id, state, price_day_total, room_id, date, reservation_id + FROM pms_reservation_line + WHERE pms_property_id = %s AND state != 'cancel' + AND occupies_availability = true AND date <= %s ) 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 + 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 @@ -317,17 +328,19 @@ def get_reservations_calendar(self, calendar_search_param): index_date_last_reservation = False for index, item in enumerate(result): date = { - "date": datetime.combine(item['date'], datetime.min.time()).isoformat(), + "date": datetime.combine(item["date"], datetime.min.time()).isoformat(), "reservationLines": [], } restriction = build_restriction(item) if restriction: - date.update({ - "restriction": restriction, - }) + date.update( + { + "restriction": restriction, + } + ) - if last_room_id != item['room_id']: - last_room_id = item['room_id'] + if last_room_id != item["room_id"]: + last_room_id = item["room_id"] last_reservation_id = False response.append( CalendarRenderInfo( @@ -335,17 +348,16 @@ def get_reservations_calendar(self, calendar_search_param): capacity=item["capacity"], roomTypeClassId=item["room_type_class_id"], roomTypeId=item["room_type_id"], - dates=[ - date - ], + 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( + 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 @@ -356,10 +368,15 @@ def get_reservations_calendar(self, calendar_search_param): else result[index + 1], ) ) - last_reservation_id = item['reservation_id'] + 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( + 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 @@ -370,7 +387,7 @@ def get_reservations_calendar(self, calendar_search_param): else result[index + 1], ) ) - last_reservation_id = item['reservation_id'] + last_reservation_id = item["reservation_id"] else: last_reservation_id = False return response @@ -394,7 +411,7 @@ def get_prices_rules_calendar(self, calendar_search_param): date_to = datetime.strptime(calendar_search_param.dateTo, "%Y-%m-%d").date() self.env.cr.execute( - f""" + """ SELECT dr.room_type_id, dr.date date, it.id pricelist_item_id, @@ -412,7 +429,8 @@ def get_prices_rules_calendar(self, calendar_search_param): 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' + WHERE name = 'list_price' + AND model = 'product.template' ) imf WHERE ipp.model_id = imf.model_id AND ipp.field_id = imf.field_id @@ -422,7 +440,8 @@ def get_prices_rules_calendar(self, calendar_search_param): ) 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 + 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 @@ -465,7 +484,8 @@ def get_prices_rules_calendar(self, calendar_search_param): 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) + WHERE relp.product_pricelist_item_id = it.id + AND relp.pms_property_id = %s) ORDER BY dr.sequence, dr.room_type_id, dr.date; """, ( @@ -482,30 +502,30 @@ def get_prices_rules_calendar(self, calendar_search_param): ) result = self.env.cr.dictfetchall() - CalendarPricesRulesRenderInfo = self.env.datamodels["pms.calendar.prices.rules.render.info"] + CalendarPricesRulesRenderInfo = self.env.datamodels[ + "pms.calendar.prices.rules.render.info" + ] last_room_type_id = False - for index, item in enumerate(result): + 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'], + "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'], + "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'] + 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"], @@ -537,7 +557,7 @@ def get_calendar_headers(self, calendar_search_param): room_ids = tuple(calendar_search_param.roomIds) self.env.cr.execute( - f""" + """ SELECT d.date, bool_or(l.overbooking) overbooking, CEIL(SUM(l.price_day_total)) daily_billing, @@ -600,10 +620,14 @@ def get_calendar_headers(self, calendar_search_param): for item in result: response.append( CalendarHeaderInfo( - date=datetime.combine(item['date'], datetime.min.time()).isoformat(), + 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, + occupancyRate=item["occupancy_rate"] + if item["occupancy_rate"] + else 0, overbooking=item["overbooking"] if item["overbooking"] else False, ) ) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index 2d16f4890e..d997eabf70 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -1,9 +1,10 @@ -from odoo.addons.component.core import Component -from odoo.addons.base_rest import restapi -from odoo.addons.base_rest_datamodel.restapi import Datamodel -from odoo import fields 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): @@ -30,17 +31,20 @@ def get_pending_reservations(self, pms_dashboard_search_param): dateTo = fields.Date.from_string(pms_dashboard_search_param.dateTo) self.env.cr.execute( - f""" + """ SELECT d.date, - SUM(CASE WHEN r.checkin = d.date AND r.state IN ('confirm', 'arrival_delayed') THEN 1 ELSE 0 + 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 + 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 + 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 @@ -57,19 +61,30 @@ def get_pending_reservations(self, pms_dashboard_search_param): ), ) - result = self.env.cr.dictfetchall() pending_reservations = [] - DashboardPendingReservations = self.env.datamodels["pms.dashboard.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, + 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 @@ -91,7 +106,7 @@ def get_occupancy(self, pms_dashboard_search_param): date_occupancy = fields.Date.from_string(pms_dashboard_search_param.date) self.env.cr.execute( - f""" + """ SELECT CEIL(l.num * 100.00 / tr.num_total_rooms) AS occupancy FROM ( @@ -124,7 +139,8 @@ def get_occupancy(self, pms_dashboard_search_param): value=result[0]["occupancy"] if result[0]["occupancy"] else 0, ) - @restapi.method([ + @restapi.method( + [ ( [ "/state-rooms", @@ -140,7 +156,7 @@ 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( - f""" + """ SELECT d.date, COALESCE(rln.num_occupied_rooms, 0) AS num_occupied_rooms, COALESCE( rlo.num_out_of_service_rooms, 0) AS num_out_of_service_rooms, @@ -193,22 +209,29 @@ def get_state_rooms(self, pms_dashboard_search_param): 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, + 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", - ) - ], + @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", @@ -217,11 +240,12 @@ 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( - f""" + """ 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 + CEIL(COUNT(r.id) * 100.00 / tr.num_total_reservations) + AS percentage_by_sale_channel FROM ( SELECT COUNT(1) num_total_reservations @@ -235,7 +259,11 @@ def get_reservations_by_sale_channel(self, pms_dashboard_search_param): 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 + 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; """, ( @@ -250,12 +278,20 @@ def get_reservations_by_sale_channel(self, pms_dashboard_search_param): result = self.env.cr.dictfetchall() state_rooms_result = [] - DashboardReservationsBySaleChannel = self.env.datamodels["pms.dashboard.reservations.by.sale.channel"] + 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, + 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 @@ -277,7 +313,7 @@ def get_billing(self, pms_dashboard_search_param): date_billing = fields.Date.from_string(pms_dashboard_search_param.date) self.env.cr.execute( - f""" + """ 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 @@ -295,7 +331,7 @@ def get_billing(self, pms_dashboard_search_param): 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, + value=result[0]["billing"] if result[0]["billing"] else 0, ) @restapi.method( @@ -315,7 +351,9 @@ 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)]) + pms_property = self.env["pms.property"].search( + [("id", "=", pms_dashboard_search_param.pmsPropertyId)] + ) adr = pms_property._get_adr(date_from, date_to) @@ -342,7 +380,9 @@ 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)]) + pms_property = self.env["pms.property"].search( + [("id", "=", pms_dashboard_search_param.pmsPropertyId)] + ) revpar = pms_property._get_revpar(date_from, date_to) @@ -369,7 +409,7 @@ 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( - f""" + """ SELECT COUNT(1) new_folios FROM pms_folio f WHERE DATE(f.create_date) = %s @@ -407,7 +447,7 @@ def get_overnights(self, pms_dashboard_search_param): date = fields.Date.from_string(pms_dashboard_search_param.date) self.env.cr.execute( - f""" + """ SELECT COUNT(1) overnights FROM pms_reservation_line l INNER JOIN pms_reservation r ON r.id = l.reservation_id @@ -419,7 +459,6 @@ def get_overnights(self, pms_dashboard_search_param): AND r.reservation_type != 'out' """, ( - date, pms_dashboard_search_param.pmsPropertyId, ), @@ -449,7 +488,7 @@ def get_cancelled_overnights(self, pms_dashboard_search_param): date = fields.Date.from_string(pms_dashboard_search_param.date) self.env.cr.execute( - f""" + """ SELECT COUNT(1) cancelled_overnights FROM pms_reservation_line l INNER JOIN pms_reservation r ON r.id = l.reservation_id @@ -470,7 +509,9 @@ def get_cancelled_overnights(self, pms_dashboard_search_param): DashboardNumericResponse = self.env.datamodels["pms.dashboard.numeric.response"] return DashboardNumericResponse( - value=result[0]["cancelled_overnights"] if result[0]["cancelled_overnights"] else 0, + value=result[0]["cancelled_overnights"] + if result[0]["cancelled_overnights"] + else 0, ) @restapi.method( @@ -490,7 +531,7 @@ def get_overbookings(self, pms_dashboard_search_param): date = fields.Date.from_string(pms_dashboard_search_param.date) self.env.cr.execute( - f""" + """ SELECT COUNT(1) overbookings FROM pms_reservation_line l WHERE l.date = %s @@ -498,7 +539,6 @@ def get_overbookings(self, pms_dashboard_search_param): AND l.overbooking = true """, ( - date, pms_dashboard_search_param.pmsPropertyId, ), @@ -511,7 +551,8 @@ def get_overbookings(self, pms_dashboard_search_param): value=result[0]["overbookings"] if result[0]["overbookings"] else 0, ) - @restapi.method([ + @restapi.method( + [ ( [ "/occupied-rooms", @@ -519,7 +560,6 @@ def get_overbookings(self, pms_dashboard_search_param): "GET", ) ], - input_param=Datamodel("pms.dashboard.range.dates.search.param"), output_param=Datamodel("pms.dashboard.state.rooms", is_list=True), auth="jwt_api_pms", @@ -528,7 +568,7 @@ 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( - f""" + """ SELECT d.date, COALESCE(rln.num_occupied_rooms, 0) AS num_occupied_rooms FROM ( @@ -559,20 +599,25 @@ def get_occupied_rooms(self, pms_dashboard_search_param): 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, + 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", - ) - ], + @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", @@ -581,12 +626,12 @@ 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( - f""" + """ 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 + 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 @@ -612,20 +657,23 @@ def get_daily_billings(self, pms_dashboard_search_param): for item in result: result_daily_billings.append( DashboardStateRooms( - date=datetime.combine(item['date'], datetime.min.time()).isoformat(), + 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", - ), - ], + @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", @@ -633,7 +681,7 @@ def get_daily_billings(self, pms_dashboard_search_param): 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( + for folio in self.env["pms.folio"].search( [ ("first_checkin", ">=", datetime.now().date()), ("pms_property_id", "=", pms_folio_search_param.pmsPropertyId), @@ -642,7 +690,6 @@ def get_last_received_folios(self, pms_folio_search_param): offset=pms_folio_search_param.offset, order="create_date desc", ): - print(folio.id) result_folios.append( PmsFolioShortInfo( id=folio.id, @@ -674,19 +721,20 @@ def get_last_received_folios(self, pms_folio_search_param): ) return result_folios - @restapi.method([ - ( - [ - "/num-last-received-folios", - ], - "GET", - ), - ], + @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( + return self.env["pms.folio"].search_count( [ ("first_checkin", ">=", datetime.now().date()), ("pms_property_id", "=", pms_folio_search_param.pmsPropertyId), diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index fd0e70a670..17a62b68ad 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -2,6 +2,8 @@ import logging from datetime import datetime, timedelta +import pytz + from odoo import _, fields from odoo.exceptions import MissingError, ValidationError from odoo.osv import expression @@ -10,7 +12,6 @@ from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component -import pytz from ..pms_api_rest_utils import url_image_pms_api_rest @@ -133,11 +134,7 @@ def get_folios(self, folio_search_param): domain_filter = list() if folio_search_param.last: - domain_filter.append( - [ - ("checkin", ">=", fields.Date.today()) - ] - ) + domain_filter.append([("checkin", ">=", fields.Date.today())]) if folio_search_param.filter: target = folio_search_param.filter @@ -1251,7 +1248,9 @@ def get_folio_reservation_messages(self, folio_id): ] for message in messages: reservation_message_date = pytz.UTC.localize(message.date) - reservation_message_date = reservation_message_date.astimezone(user_tz) + 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 @@ -1272,7 +1271,9 @@ def get_folio_reservation_messages(self, folio_id): ).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'), + authorImageUrl=url_image_pms_api_rest( + "res.partner", message.author_id.id, "image_1024" + ), ) ) PmsFolioMessageInfo = self.env.datamodels["pms.folio.message.info"] @@ -1298,7 +1299,9 @@ def get_folio_reservation_messages(self, folio_id): ).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'), + authorImageUrl=url_image_pms_api_rest( + "res.partner", folio_message.author_id.id, "image_1024" + ), ) ) PmsMessageInfo = self.env.datamodels["pms.message.info"] diff --git a/pms_api_rest/services/pms_id_category_service.py b/pms_api_rest/services/pms_id_category_service.py index 80c50577ad..bcfe7f4a8d 100644 --- a/pms_api_rest/services/pms_id_category_service.py +++ b/pms_api_rest/services/pms_id_category_service.py @@ -24,7 +24,11 @@ class PmsIdCategoryService(Component): 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).search([]): + for id_category in ( + self.env["res.partner.id_category"] + .with_context(lang=self.env.user.lang) + .search([]) + ): result_id_categories.append( PmsIdCategoryInfo( id=id_category.id, diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index 744e3fc1d3..a79feebf57 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -9,8 +9,10 @@ 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" @@ -81,7 +83,9 @@ def login(self, user): 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'), + 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 index 8f42a8118b..be728b3b08 100644 --- a/pms_api_rest/services/pms_notification_service.py +++ b/pms_api_rest/services/pms_notification_service.py @@ -1,6 +1,7 @@ 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 diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 85fda3ed69..08cbe5b7e3 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -1,66 +1,68 @@ import re -from datetime import datetime, date, timedelta +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 -from odoo.exceptions import ValidationError -from odoo import _ _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', + "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" @@ -83,7 +85,6 @@ class PmsPartnerService(Component): def get_partners(self, pms_partner_search_params): result_partners = [] domain = [] - print("pms_partner_search_params", pms_partner_search_params) if pms_partner_search_params.housedNow: partners_housed_now = ( @@ -99,14 +100,26 @@ def get_partners(self, pms_partner_search_params): 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), + "|", + "&", + ("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") + ) + .mapped("partner_id") ) domain.append(("id", "in", partners_housed_last_week.ids)) if pms_partner_search_params.housedLastMonth: @@ -116,14 +129,26 @@ def get_partners(self, pms_partner_search_params): 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), + "|", + "&", + ("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") + ) + .mapped("partner_id") ) domain.append(("id", "in", partners_housed_last_month.ids)) if ( @@ -325,7 +350,9 @@ def get_partner_payments(self, partner_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, + destinationJournalId=destination_journal_id + if destination_journal_id + else None, date=datetime.combine( payment.date, datetime.min.time() ).isoformat(), @@ -373,18 +400,14 @@ def get_partner_invoices(self, partner_id): 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, + 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, + discount=move_line.discount if move_line.discount else None, displayType=move_line.display_type if move_line.display_type else None, @@ -420,15 +443,11 @@ def get_partner_invoices(self, partner_id): else None, date=invoice_date, state=move.state if move.state else None, - paymentState=move.payment_state - if move.payment_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, + 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, @@ -467,7 +486,6 @@ def get_partner_by_doc_number(self, document_type, document_number): lambda doc: doc.category_id.id == doc_type.id ) - PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] document_expedition_date = False @@ -475,9 +493,7 @@ def get_partner_by_doc_number(self, document_type, document_number): document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") birthdate_date = False if partner.birthdate_date: - birthdate_date = partner.birthdate_date.strftime( - "%d/%m/%Y" - ) + birthdate_date = partner.birthdate_date.strftime("%d/%m/%Y") partners.append( PmsCheckinPartnerInfo( partnerId=partner.id or None, @@ -507,7 +523,8 @@ def get_partner_by_doc_number(self, document_type, document_number): [ ( [ - "/check-doc-number///", + "/check-doc-number//" + "/", ], "GET", ) @@ -517,8 +534,8 @@ def get_partner_by_doc_number(self, document_type, document_number): # 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'].browse(country_id) - document_type = self.env['res.partner.id_category'].browse(document_type_id) + country = self.env["res.country"].browse(country_id) + document_type = self.env["res.partner.id_category"].browse(document_type_id) id_number = self.env["res.partner.id_number"].new( { "name": document_number, @@ -536,7 +553,9 @@ def check_document_number(self, document_number, document_type_id, country_id): vat_number=document_number, ) if error: - error_mens = self._construct_check_vat_error_msg(vat_number=document_number, country_code=country.code) + error_mens = self._construct_check_vat_error_msg( + vat_number=document_number, country_code=country.code + ) if error_mens: raise ValidationError(error_mens) @@ -742,18 +761,20 @@ 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']) + 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.', + 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 + format=vat_no, ) - return '\n' + _( - 'The VAT number [%(vat)s] does not seem to be valid. \nNote: the expected format is %(format)s', + 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 + format=vat_no, ) diff --git a/pms_api_rest/services/pms_price_service.py b/pms_api_rest/services/pms_price_service.py index 4b56b61da8..f74a3b6d03 100644 --- a/pms_api_rest/services/pms_price_service.py +++ b/pms_api_rest/services/pms_price_service.py @@ -49,11 +49,7 @@ def get_prices(self, prices_search_param): [("id", "=", prices_search_param.boardServiceId)] ) else: - raise MissingError( - _( - "Wrong input param" - ) - ) + raise MissingError(_("Wrong input param")) PmsPriceInfo = self.env.datamodels["pms.price.info"] result_prices = [] @@ -154,7 +150,7 @@ def _get_board_service_price( ): price = 0 if product_id: - products = self.env['product.product'].browse(product_id) + products = self.env["product.product"].browse(product_id) else: products = board_service.board_service_line_ids.mapped("product_id") for product in products: diff --git a/pms_api_rest/services/pms_pricelist_service.py b/pms_api_rest/services/pms_pricelist_service.py index 6f7965a271..e36840e9b6 100644 --- a/pms_api_rest/services/pms_pricelist_service.py +++ b/pms_api_rest/services/pms_pricelist_service.py @@ -32,7 +32,7 @@ def get_pricelists(self, pms_search_param, **args): ("is_pms_available", "=", True), ] if pms_search_param.daily and pms_search_param.daily is True: - domain.append(("pricelist_type", "=", 'daily')) + domain.append(("pricelist_type", "=", "daily")) pricelists = self.env["product.pricelist"].search(domain) if pms_search_param.pmsPropertyIds and pms_search_param.pmsPropertyId: @@ -192,7 +192,7 @@ def _create_or_update_pricelist_items(self, pms_pricelist_item_info): input_param=Datamodel("pms.pricelist.items.info", is_list=False), auth="jwt_api_pms", ) - def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): + def create_pricelist_item_old(self, pricelist_id, pms_pricelist_item_info): self._create_or_update_pricelist_items(pms_pricelist_item_info) @restapi.method( @@ -207,10 +207,14 @@ def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): input_param=Datamodel("pms.pricelist.items.info", is_list=False), auth="jwt_api_pms", ) - def create_pricelist_item(self, pricelist_id, pms_pricelist_item_info): - pricelist_ids = list({item.pricelistId for item in pms_pricelist_item_info.pricelistItems}) + 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.") + raise ValidationError( + _("You cannot create pricelist items for different pricelists at once.") + ) else: self._create_or_update_pricelist_items(pms_pricelist_item_info) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index 77986da2e6..daea48b1a2 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -1,6 +1,7 @@ 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 @@ -31,7 +32,11 @@ def get_properties(self): ): state_name = False if prop.state_id: - state_name = self.env['res.country.state'].search([('id', '=', prop.state_id.id)]).name + state_name = ( + self.env["res.country.state"] + .search([("id", "=", prop.state_id.id)]) + .name + ) result_properties.append( PmsPropertyInfo( id=prop.id, @@ -53,7 +58,9 @@ def get_properties(self): simpleInColor=prop.simple_in_color, simpleFutureColor=prop.simple_future_color, language=prop.lang, - hotelImageUrl=url_image_pms_api_rest('pms.property', prop.id, 'hotel_image_pms_api_rest'), + hotelImageUrl=url_image_pms_api_rest( + "pms.property", prop.id, "hotel_image_pms_api_rest" + ), ) ) return result_properties diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 50e44e59e9..f789482951 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -127,11 +127,15 @@ def get_reservation(self, reservation_id, pms_search_param): numServices=len(reservation.service_ids) if reservation.service_ids else 0, - isReselling=any(line.is_reselling for line in reservation.reservation_line_ids), - ) + isReselling=any( + line.is_reselling for line in reservation.reservation_line_ids + ), + ) return res - def _create_vals_from_params(self, reservation_vals, reservation_data, reservation_id): + 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} @@ -158,7 +162,6 @@ def _create_vals_from_params(self, reservation_vals, reservation_data, reservati if reservation_data.checkout: reservation_vals.update({"checkout": reservation_data.checkout}) - return reservation_vals @restapi.method( @@ -231,8 +234,13 @@ def update_reservation(self, reservation_id, reservation_data): ) 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): + 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: @@ -261,18 +269,19 @@ def update_reservation(self, reservation_id, reservation_data): "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: - if reservation_data.boardServices: - reservation.with_context(skip_compute_service_ids=True).write(reservation_vals) - else: - reservation.write(reservation_vals) - # print(reservation.service_ids.mapped("name")) - + # if service_cmds: + # reservation_vals.update({"service_ids": service_cmds}) + # if reservation_vals: + # if reservation_data.boardServices: + # reservation.with_context(skip_compute_service_ids=True).write( + # reservation_vals + # ) + # else: + # reservation.write(reservation_vals) + # # print(reservation.service_ids.mapped("name")) def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): # Return dict witch reservation.lines values (only modified if line exist, @@ -542,7 +551,7 @@ def get_checkin_partners(self, reservation_id): pass else: # TODO Review state draft - #.filtered( + # .filtered( # lambda ch: ch.state != "dummy" # ) for checkin_partner in reservation.checkin_partner_ids: diff --git a/pms_api_rest/services/pms_room_service.py b/pms_api_rest/services/pms_room_service.py index 5f146cdc0a..9a62bc9a01 100644 --- a/pms_api_rest/services/pms_room_service.py +++ b/pms_api_rest/services/pms_room_service.py @@ -97,8 +97,13 @@ def get_rooms(self, room_search_param): 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 '' + 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 diff --git a/pms_api_rest/services/pms_room_type_class_service.py b/pms_api_rest/services/pms_room_type_class_service.py index 2d149623af..8e9d5498ca 100644 --- a/pms_api_rest/services/pms_room_type_class_service.py +++ b/pms_api_rest/services/pms_room_type_class_service.py @@ -1,8 +1,10 @@ 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" @@ -51,7 +53,9 @@ def get_room_type_class(self, room_type_class_search_param): 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'), + 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_sale_channel_service.py b/pms_api_rest/services/pms_sale_channel_service.py index 70fb514ea5..fa8dd999be 100644 --- a/pms_api_rest/services/pms_sale_channel_service.py +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -4,8 +4,10 @@ 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" @@ -62,7 +64,9 @@ def get_sale_channels(self, sale_channel_search_param): 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'), + iconUrl=url_image_pms_api_rest( + "pms.sale.channel", sale_channel.id, "icon" + ), ) ) return result_sale_channels diff --git a/pms_api_rest/services/pms_transaction_service.py b/pms_api_rest/services/pms_transaction_service.py index 4c11c13327..7ebc33e89c 100644 --- a/pms_api_rest/services/pms_transaction_service.py +++ b/pms_api_rest/services/pms_transaction_service.py @@ -85,11 +85,15 @@ def get_transactions(self, pms_transactions_search_param): 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], - ]) + 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) diff --git a/pms_api_rest/services/res_city_zip_service.py b/pms_api_rest/services/res_city_zip_service.py index 195045dbb0..6146436d91 100644 --- a/pms_api_rest/services/res_city_zip_service.py +++ b/pms_api_rest/services/res_city_zip_service.py @@ -9,7 +9,6 @@ class ResCityZipService(Component): _usage = "zips" _collection = "pms.services" - @restapi.method( [ ( @@ -28,7 +27,9 @@ def get_address_data(self, zip_search_param): 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"].search([("display_name", "ilike", zip_search_param.address)], limit=10) + res_zip = self.env["res.city.zip"].search( + [("display_name", "ilike", zip_search_param.address)], limit=10 + ) if res_zip: for address in res_zip: @@ -44,7 +45,6 @@ def get_address_data(self, zip_search_param): ) return result_res_zip - @restapi.method( [ ( diff --git a/pms_api_rest/services/res_country_service.py b/pms_api_rest/services/res_country_service.py index 206d0cb3fe..18b7748fc0 100644 --- a/pms_api_rest/services/res_country_service.py +++ b/pms_api_rest/services/res_country_service.py @@ -24,7 +24,9 @@ class ResCountryService(Component): 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).search([]): + for country in ( + self.env["res.country"].with_context(lang=self.env.user.lang).search([]) + ): result_countries.append( ResCountriesInfo( id=country.id, diff --git a/pms_api_rest/services/res_partner_category_service.py b/pms_api_rest/services/res_partner_category_service.py index ee3efc3717..000373b21a 100644 --- a/pms_api_rest/services/res_partner_category_service.py +++ b/pms_api_rest/services/res_partner_category_service.py @@ -24,7 +24,11 @@ class PmsPartnerCategoriesService(Component): 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([]): + for category in ( + self.env["res.partner.category"] + .with_context(lang=self.env.user.lang) + .search([]) + ): result_categories.append( ResPartnerCategoryInfo( id=category.id, diff --git a/pms_api_rest/views/pms_property_views.xml b/pms_api_rest/views/pms_property_views.xml index b47007b356..39b4a2bfca 100644 --- a/pms_api_rest/views/pms_property_views.xml +++ b/pms_api_rest/views/pms_property_views.xml @@ -4,9 +4,16 @@ 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 index 3434234658..c3fbae7f60 100644 --- a/pms_api_rest/views/pms_room_type_class_views.xml +++ b/pms_api_rest/views/pms_room_type_class_views.xml @@ -5,7 +5,7 @@ - + From c2bad12075c5e135918437a9ee6197a3dda3b4ca Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 20 Dec 2023 11:53:21 +0100 Subject: [PATCH 473/547] [FIX] pms-api-rest: swap createHour -> createDate @ folio short info (datamodel) --- pms_api_rest/datamodels/pms_folio.py | 2 +- pms_api_rest/services/pms_dashboard_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 80385bd94c..20891c53d2 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -75,4 +75,4 @@ class PmsFolioShortInfo(Datamodel): saleChannelId = fields.Integer(required=False, allow_none=True) firstCheckin = fields.String(required=False, allow_none=True) lastCheckout = fields.String(required=False, allow_none=True) - createHour = fields.String(required=False, allow_none=True) + createDate = fields.String(required=False, allow_none=True) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index d997eabf70..e3b280f0ed 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -716,7 +716,7 @@ def get_last_received_folios(self, pms_folio_search_param): 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 From 689832a9853dcb72bcb864630ba0d4108519b9c3 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Fri, 22 Dec 2023 16:09:00 +0100 Subject: [PATCH 474/547] [FIX] pms-api-rest: createHour field @ PmsFolioShortInfo (datamodel) --- pms_api_rest/datamodels/pms_folio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 20891c53d2..91c0943e69 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -76,3 +76,4 @@ class PmsFolioShortInfo(Datamodel): 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) From 4c382919f861e454807f4056536c7f95d8645655 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 22 Dec 2023 20:34:54 +0100 Subject: [PATCH 475/547] [FIX]14.0-pms_api_rest: write reservation_vals in reservation service patch --- .../services/pms_reservation_service.py | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index f789482951..f6b3ec72f7 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -217,15 +217,7 @@ def update_reservation(self, reservation_id, reservation_data): "reservation_line_ids": reservation_lines_vals, } ) - - 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.confirm() - if reservation_data.toCheckout is not None and reservation_data.toCheckout: - reservation.action_reservation_checkout() + self._update_reservation_state(reservation, reservation_data) reservation_vals = self._create_vals_from_params( reservation_vals, @@ -272,16 +264,25 @@ def update_reservation(self, reservation_id, reservation_data): }, ) ) - # if service_cmds: - # reservation_vals.update({"service_ids": service_cmds}) - # if reservation_vals: - # if reservation_data.boardServices: - # reservation.with_context(skip_compute_service_ids=True).write( - # reservation_vals - # ) - # else: - # reservation.write(reservation_vals) - # # print(reservation.service_ids.mapped("name")) + if service_cmds: + reservation_vals.update({"service_ids": service_cmds}) + + if reservation_vals and reservation_data.boardServices: + reservation.with_context(skip_compute_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.confirm() + if reservation_data.toCheckout is not None and reservation_data.toCheckout: + reservation.action_reservation_checkout() def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): # Return dict witch reservation.lines values (only modified if line exist, From 87603178862b97b4d19b6ab8b03ced53089c81f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 23 Dec 2023 09:35:09 +0100 Subject: [PATCH 476/547] [FIX]pms_api_rest: model product.template --- pms_api_rest/models/__init__.py | 1 + .../models/{product_product.py => product_template.py} | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename pms_api_rest/models/{product_product.py => product_template.py} (75%) diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 3a33b55647..09f08a5897 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -4,3 +4,4 @@ from . import sql_export from . import pms_room_type_class from . import account_bank_statement +from . import product_template diff --git a/pms_api_rest/models/product_product.py b/pms_api_rest/models/product_template.py similarity index 75% rename from pms_api_rest/models/product_product.py rename to pms_api_rest/models/product_template.py index 57c30338a5..54cf34b0da 100644 --- a/pms_api_rest/models/product_product.py +++ b/pms_api_rest/models/product_template.py @@ -1,8 +1,8 @@ from odoo import fields, models -class ProductProduct(models.Model): - _inherit = "product.product" +class ProductTemplate(models.Model): + _inherit = "product.template" channel_available = fields.Boolean( string="Sale Channel Available", From b103626ab2422faae2e60858f08d6c301c5b75bd Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 8 Jan 2024 10:25:41 +0100 Subject: [PATCH 477/547] [FIX] pms_api_rest: filter last received reservations 'normal' - dashboard service --- pms_api_rest/services/pms_dashboard_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index e3b280f0ed..34966cf114 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -685,6 +685,7 @@ def get_last_received_folios(self, pms_folio_search_param): [ ("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, @@ -738,5 +739,6 @@ def get_num_last_received_folios(self, pms_folio_search_param): [ ("first_checkin", ">=", datetime.now().date()), ("pms_property_id", "=", pms_folio_search_param.pmsPropertyId), + ("reservation_type", "=", "normal"), ], ) From 88152f41f756d91447fec83c5aabf91dacd924c6 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Mon, 8 Jan 2024 10:26:10 +0100 Subject: [PATCH 478/547] [IMP] pms-api-rest: add image url in user service --- pms_api_rest/services/pms_user_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pms_api_rest/services/pms_user_service.py b/pms_api_rest/services/pms_user_service.py index 9d550076a1..0b70063a3c 100644 --- a/pms_api_rest/services/pms_user_service.py +++ b/pms_api_rest/services/pms_user_service.py @@ -12,6 +12,8 @@ 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" @@ -42,6 +44,9 @@ def get_user(self, user_id): 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, ) From 8e76713ecfd12611ceb3ce97c996db5cc3f8e958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 23 Jan 2024 09:26:32 +0100 Subject: [PATCH 479/547] [IMP]pms_api_rest: compute boardservice default included in pricelist --- pms_api_rest/services/pms_folio_service.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 17a62b68ad..6fbdf4ed15 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -655,8 +655,13 @@ def create_folio(self, pms_folio_info): 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: - reservation_record._compute_board_service_room_id() + 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 + )._compute_board_service_room_id() 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 @@ -1403,10 +1408,10 @@ def get_board_service_room_type_id( 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 """ - if self.get_api_client_type() == "internal_app": - return board_service_id board_service = self.env["pms.board.service"].browse(board_service_id) room_type = self.env["pms.room.type"].browse(room_type_id) + if self.get_api_client_type() == "internal_app": + return board_service_id if board_service and room_type: return ( self.env["pms.board.service.room.type"] From e102c30a5ad0143d50bc5ee1b053b633b874e417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 26 Jan 2024 11:44:32 +0100 Subject: [PATCH 480/547] [IMP]pms_api_rest: avoid send innecesary pwd param in tokens payload --- pms_api_rest/services/pms_login_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index a79feebf57..ccfeaffcf4 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -61,7 +61,6 @@ def login(self, user): "iss": "pms", "exp": timestamp_expire_in_a_min, "username": user.username, - "password": user.password, }, key=validator.secret_key, algorithm=validator.secret_algorithm, From e19587805e49b5b07a085cb830ab9204486253d7 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Tue, 6 Feb 2024 19:31:18 +0000 Subject: [PATCH 481/547] [IMP] pms-api-rest: add field 'createdBy' to folio & reservation service --- pms_api_rest/datamodels/pms_folio.py | 2 ++ pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/services/pms_folio_service.py | 1 + pms_api_rest/services/pms_reservation_service.py | 2 ++ 4 files changed, 6 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 91c0943e69..271489d8d4 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -29,6 +29,8 @@ class PmsFolioInfo(Datamodel): 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( diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index e396172e60..9bf4da99bd 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -91,6 +91,7 @@ class PmsReservationInfo(Datamodel): partnerRequests = fields.String(required=False, allow_none=True) nights = fields.Integer(required=False, allow_none=True) isReselling = fields.Boolean(required=False, allow_none=True) + createdBy = fields.String(required=False, allow_none=True) # TODO: Refact # messages = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 6fbdf4ed15..3ce6a51ccd 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -62,6 +62,7 @@ def get_folio(self, folio_id): 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, diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index f6b3ec72f7..6ee3af0378 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -103,6 +103,7 @@ def get_reservation(self, reservation_id, pms_search_param): 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, @@ -130,6 +131,7 @@ def get_reservation(self, reservation_id, pms_search_param): isReselling=any( line.is_reselling for line in reservation.reservation_line_ids ), + ) return res From 39bc23b18227413cff9f3caf49c72598a7e3c534 Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 5 Feb 2024 18:59:17 +0100 Subject: [PATCH 482/547] [IMP]14.0-pms_api_rest: added new folio filters in get_folios --- .../services/pms_dashboard_service.py | 1 + pms_api_rest/services/pms_folio_service.py | 58 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index 34966cf114..f4ea451deb 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -30,6 +30,7 @@ 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 diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 3ce6a51ccd..afca0ed680 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -150,7 +150,63 @@ def get_folios(self, folio_search_param): ] domain_filter.append(expression.OR(subdomains)) if folio_search_param.filterByState: - if folio_search_param.filterByState == "byCheckin": + 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())], From b58706fe36965a6cb3ed11035522f41a8369767f Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 24 Jan 2024 18:33:45 +0000 Subject: [PATCH 483/547] [IMP] pms-api-rest: adapt services & datamodels (folio, reservation, reservation_line) to swapping mode requirements --- pms_api_rest/datamodels/pms_folio.py | 1 + .../datamodels/pms_reservation_line.py | 5 ++ pms_api_rest/datamodels/pms_search_param.py | 1 + pms_api_rest/services/pms_folio_service.py | 3 ++ .../services/pms_reservation_line_service.py | 51 +++++++++++++++++++ .../services/pms_reservation_service.py | 2 + 6 files changed, 63 insertions(+) diff --git a/pms_api_rest/datamodels/pms_folio.py b/pms_api_rest/datamodels/pms_folio.py index 271489d8d4..61267a2ba9 100644 --- a/pms_api_rest/datamodels/pms_folio.py +++ b/pms_api_rest/datamodels/pms_folio.py @@ -13,6 +13,7 @@ class PmsFolioSearchParam(Datamodel): 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) class PmsFolioInfo(Datamodel): diff --git a/pms_api_rest/datamodels/pms_reservation_line.py b/pms_api_rest/datamodels/pms_reservation_line.py index f7597b6844..cfecb29107 100644 --- a/pms_api_rest/datamodels/pms_reservation_line.py +++ b/pms_api_rest/datamodels/pms_reservation_line.py @@ -6,6 +6,8 @@ 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) @@ -23,3 +25,6 @@ class PmsReservationLineInfo(Datamodel): reservationId = fields.Integer(required=False, allow_none=False) pmsPropertyId = fields.Integer(required=False, allow_none=False) 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_search_param.py b/pms_api_rest/datamodels/pms_search_param.py index dc7f79001a..78474734b2 100644 --- a/pms_api_rest/datamodels/pms_search_param.py +++ b/pms_api_rest/datamodels/pms_search_param.py @@ -9,3 +9,4 @@ class PmsSearchParam(Datamodel): pmsPropertyId = fields.Integer(required=False, allow_none=True) pmsPropertyIds = fields.List(fields.Integer(), required=False) toAssign = fields.Boolean(required=False, allow_none=True) + ids = fields.List(fields.Integer(), required=False) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index afca0ed680..b913cbbf5b 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -137,6 +137,9 @@ def get_folios(self, folio_search_param): 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: diff --git a/pms_api_rest/services/pms_reservation_line_service.py b/pms_api_rest/services/pms_reservation_line_service.py index 5c5c0756e6..0cb9d80498 100644 --- a/pms_api_rest/services/pms_reservation_line_service.py +++ b/pms_api_rest/services/pms_reservation_line_service.py @@ -48,6 +48,57 @@ def get_reservation_line(self, reservation_line_id): 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): + print(reservation_line.state) + 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( # [ # ( diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 6ee3af0378..ff7c161e17 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -673,6 +673,8 @@ def get_reservations(self, pms_search_param): 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)) reservations = self.env["pms.reservation"].search(domain) PmsReservationInfo = self.env.datamodels["pms.reservation.info"] if not reservations: From 2c9d00ccfdffc91c695c8884df7dd2909903263a Mon Sep 17 00:00:00 2001 From: braisab Date: Tue, 26 Dec 2023 13:01:31 +0100 Subject: [PATCH 484/547] [IMP]14.0-pms_api_rest: segmentations are filtered by the is_used_in_checkin field in service --- pms_api_rest/services/res_partner_category_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/res_partner_category_service.py b/pms_api_rest/services/res_partner_category_service.py index 000373b21a..d63bfa00a0 100644 --- a/pms_api_rest/services/res_partner_category_service.py +++ b/pms_api_rest/services/res_partner_category_service.py @@ -27,7 +27,7 @@ def get_categories(self): for category in ( self.env["res.partner.category"] .with_context(lang=self.env.user.lang) - .search([]) + .search([("is_used_in_checkin", "=", True)]) ): result_categories.append( ResPartnerCategoryInfo( From 72113640dbcf28e7b7897dc26440e72194df47b2 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 30 Oct 2023 12:14:46 +0100 Subject: [PATCH 485/547] [IMP] pms_api_rest: add reservation wizard state service --- pms_api_rest/datamodels/__init__.py | 3 + pms_api_rest/datamodels/pms_wizard_state.py | 9 + .../services/pms_reservation_service.py | 207 ++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_wizard_state.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index 726e661970..df2ed820ab 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -62,3 +62,6 @@ from . import pms_avail from . import pms_dashboard from . import feed_post + +from . import pms_wizard_state + 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..f5625b6e34 --- /dev/null +++ b/pms_api_rest/datamodels/pms_wizard_state.py @@ -0,0 +1,9 @@ +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/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index ff7c161e17..7de2c26f28 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -7,6 +7,8 @@ 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.odoo.osv import expression +from odoo.odoo.tools.safe_eval import safe_eval class PmsReservationService(Component): @@ -1060,3 +1062,208 @@ def departures_report(self, pms_report_search_param): 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)]) + wizard_states = [ + { + "code": "overbooking_with_availability", + "title": "Overbooking", + "domain": "[" + "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " + "('overbooking', '=', True), " + "('reservation_type', 'in', ['normal', 'staff'])]", + "text": f"Parece que ha entrado una reserva sin haber disponibilidad para {reservation.room_type_id.name}", + "priority": 100, + }, + { + "code": "overbooking_without_availability", + "title": "Overbooking", + "domain": "[" + "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " + "('overbooking', '=', True), " + "('reservation_type', 'in', ['normal', 'staff'])]", + "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", + "title": "Divididas", + "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," + "('splitted', '=', True)," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", + "text": "", + "priority": 200, + }, + { + "code": "to_assign", + "title": "Por asignar", + "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," + "('to_assign', '=', True)," + "('reservation_type', 'in', ['normal', 'staff'])," + # "('checkin', '==', today)," + # "('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')," + # "('checkin', '==', today)," + # "('checkin', '>', today)," + "('reservation_type', '=', 'normal')," + "]", + "text": f"La reserva de {reservation.partner_name} está pendiente de confirmar, puedes confirmarla desde aquí" + "o en caso contrario podrías cancelar la reserva", + "priority": 400, + }, + { + "code": "checkin_done_precheckin", + "title": "Entrada Hoy", + "domain": "[('state', '=', 'confirm')," + # "('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')," + # "('checkin', '=', today)," + "('ratio_checkin_data', '>', 0)," + "('ratio_checkin_data','<', 100)," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", + "text": f"Faltan {reservation.pending_checkin_data} huéspedes por registrar sus datos," + f"puedes marcar la entrada de la habitación [Marcar entrada] y/o abrir el asistente de checkin" + f"para completar los huéspedes", + "priority": 530, + }, + { + "code": "checkin_no_precheckin", + "title": "Entrada Hoy", + "domain": "[('state', '=', 'confirm')," + # "('checkin', '=', today)," + "('ration_checkin_data','=', 100)," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", + "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'])," + # "('checkin', '>', today)," + "('pending_checkin_data', '>', 0)," + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", + "text": "REVISAR", + "priority": 600, + }, + { + "code": "confirmed_without_payment", + "title": "Confirmadas a futuro sin pagar", + "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," + "('reservation_type', 'in', ['normal', 'staff'])," + # "('checkin', '>', today)," + "('pending_checkin_data', '=', 0)," + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", + "text": "REVISAR", + "priority": 630, + }, + { + "code": "confirmed_without_precheckin", + "title": "Confirmadas a futuro sin pagar", + "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," + "('reservation_type', 'in', ['normal', 'staff'])," + # "('checkin', '>', today)," + "('pending_checkin_data', '>', 0)," + "('folio_payment_state', 'in', ['paid', 'overpayment','nothing_to_pay'])" + "]", + "text": "REVISAR", + "priority": 660, + }, + { + "code": "cancelled", + "title": "Cancelada con cargos y sin cobrar", + "domain": "[('state', '=', 'cancel')," + "('cancelled_reason', 'in',['late','noshow'])," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", + "text": f"La reserva de {reservation.partner_name} ha sido cancelada con una penalización de [penalizacion]," + "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} que aparentemente deberia " + f"efectuar el {reservation.agency_id if reservation.folio_id.invoice_to_agency else reservation.partner_id.name}. 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'])," + # "('checkout', '=', datetime.today())," + "]", + "text": "Reserva lista para el checkout, marca la salida directamente desde aquí.", + "priority": 10, + }, + ] + + sorted_wizard_states = sorted(wizard_states, key=lambda x: x['priority']) + PmsWizardStateInfo = self.env.datamodels["pms.wizard.state.info"] + taoday = datetime.today() + for state in sorted_wizard_states: + + domain = expression.AND([ + [("id", "=", reservation_id)], + safe_eval(state["domain"]) + ]) + + if self.env["pms.reservation"].search_count(domain): + print(state) + return PmsWizardStateInfo( + code=state["code"], + title=state["title"], + text=state["text"], + ) + + From 09ff1f04af647a0c9b9dea2721353efc7a855394 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Fri, 10 Nov 2023 17:11:09 +0100 Subject: [PATCH 486/547] [IMP] pms_api_rest: wizard state service done --- pms_api_rest/datamodels/pms_service.py | 1 + .../services/pms_reservation_service.py | 133 +++++++++++------- 2 files changed, 86 insertions(+), 48 deletions(-) diff --git a/pms_api_rest/datamodels/pms_service.py b/pms_api_rest/datamodels/pms_service.py index 004b72dffb..e7910a6b90 100644 --- a/pms_api_rest/datamodels/pms_service.py +++ b/pms_api_rest/datamodels/pms_service.py @@ -18,3 +18,4 @@ class PmsServiceInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 7de2c26f28..52b130283b 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -7,8 +7,8 @@ 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.odoo.osv import expression -from odoo.odoo.tools.safe_eval import safe_eval +from odoo.osv import expression +from odoo.tools.safe_eval import safe_eval class PmsReservationService(Component): @@ -490,6 +490,7 @@ def get_reservation_services(self, reservation_id): discount=round(service.discount, 2), isBoardService=service.is_board_service, serviceLines=service_lines, + isCancelPenalty=service.is_cancel_penalty, ) ) return result_services @@ -1077,6 +1078,7 @@ def departures_report(self, pms_report_search_param): ) 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", @@ -1084,8 +1086,11 @@ def wizard_states(self, reservation_id): "domain": "[" "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " "('overbooking', '=', True), " - "('reservation_type', 'in', ['normal', 'staff'])]", - "text": f"Parece que ha entrado una reserva sin haber disponibilidad para {reservation.room_type_id.name}", + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", + "filtered": "lambda r: r.count_alternative_free_rooms", + "text": f"Parece que ha entrado una reserva sin haber disponibilidad para {reservation.room_type_id.name}.", "priority": 100, }, { @@ -1094,82 +1099,101 @@ def wizard_states(self, reservation_id): "domain": "[" "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " "('overbooking', '=', True), " - "('reservation_type', 'in', ['normal', 'staff'])]", + 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", + "code": "splitted_without_availability", "title": "Divididas", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," "('splitted', '=', True)," + f"('checkin', '>=', '{today}')," "('reservation_type', 'in', ['normal', 'staff'])" "]", - "text": "", + "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'])," - # "('checkin', '==', today)," - # "('checkin', '>', today)," + f"('checkin', '>=', '{today}')," "]", "text": f"La reserva de {reservation.partner_name} ha sido asignada a la habitación {reservation.preferred_room_id.name}," - "puedes confirmar la habitación o cambiar a otra desde aquí.", + " puedes confirmar la habitación o cambiar a otra desde aquí.", "priority": 300, }, { "code": "to_confirm", "title": "Por confirmar", "domain": "[('state', '=', 'draft')," - # "('checkin', '==', today)," - # "('checkin', '>', today)," - "('reservation_type', '=', 'normal')," + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])," "]", - "text": f"La reserva de {reservation.partner_name} está pendiente de confirmar, puedes confirmarla desde aquí" - "o en caso contrario podrías cancelar la reserva", + "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')," - # "('checkin', '=', today)," + f"('checkin', '=', '{today}')," "('pending_checkin_data', '=', 0)," "('reservation_type', 'in', ['normal', 'staff'])" "]", "text": "Todos los huéspedes de esta reserva tienen los datos registrados, " - "puedes marcar la entrada directamente desde aquí", + " puedes marcar la entrada directamente desde aquí", "priority": 500, }, { "code": "checkin_partial_precheckin", "title": "Entrada Hoy", "domain": "[('state', '=', 'confirm')," - # "('checkin', '=', today)," - "('ratio_checkin_data', '>', 0)," - "('ratio_checkin_data','<', 100)," + 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éspedes por registrar sus datos," - f"puedes marcar la entrada de la habitación [Marcar entrada] y/o abrir el asistente de checkin" - f"para completar los huéspedes", + "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')," - # "('checkin', '=', today)," - "('ration_checkin_data','=', 100)," + "domain": "[('state', '=', 'confirm')," + f"('checkin', '=', '{today}')," + "('pending_checkin_data', '>', 0)," "('reservation_type', 'in', ['normal', 'staff'])" "]", - "text": "Registra los datos de los huéspedes desde el asistente del checkin", + "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, }, { @@ -1177,11 +1201,12 @@ def wizard_states(self, reservation_id): "title": "Confirmadas a futuro sin pagar y sin precheckin realizado", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," "('reservation_type', 'in', ['normal', 'staff'])," - # "('checkin', '>', today)," + f"('checkin', '>', '{today}')," "('pending_checkin_data', '>', 0)," "('folio_payment_state', 'in', ['not_paid', 'partial'])" "]", - "text": "REVISAR", + "text": "Esta reserva está pendiente de cobro y de que los huéspedes " + " registren sus datos: puedes enviarles un recordatorio desde aquí", "priority": 600, }, { @@ -1189,11 +1214,11 @@ def wizard_states(self, reservation_id): "title": "Confirmadas a futuro sin pagar", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," "('reservation_type', 'in', ['normal', 'staff'])," - # "('checkin', '>', today)," + f"('checkin', '>', '{today}')," "('pending_checkin_data', '=', 0)," "('folio_payment_state', 'in', ['not_paid', 'partial'])" "]", - "text": "REVISAR", + "text": "Esta reserva está pendiente de cobro, puedes enviarle sun recordatorio desde aquí", "priority": 630, }, { @@ -1201,11 +1226,11 @@ def wizard_states(self, reservation_id): "title": "Confirmadas a futuro sin pagar", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," "('reservation_type', 'in', ['normal', 'staff'])," - # "('checkin', '>', today)," + f"('checkin', '>', '{today}')," "('pending_checkin_data', '>', 0)," "('folio_payment_state', 'in', ['paid', 'overpayment','nothing_to_pay'])" "]", - "text": "REVISAR", + "text": "Esta reserva no tiene los datos de los huéspedes registrados, puedes enviarles un recordatorio desde aquí", "priority": 660, }, { @@ -1213,10 +1238,11 @@ def wizard_states(self, reservation_id): "title": "Cancelada con cargos y sin cobrar", "domain": "[('state', '=', 'cancel')," "('cancelled_reason', 'in',['late','noshow'])," - "('reservation_type', 'in', ['normal', 'staff'])" + "('folio_payment_state', 'in', ['not_paid', 'partial'])," "]", - "text": f"La reserva de {reservation.partner_name} ha sido cancelada con una penalización de [penalizacion]," - "puedes eliminar la penalización en caso de que no se vaya a cobrar...", + "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, }, { @@ -1225,45 +1251,56 @@ def wizard_states(self, reservation_id): "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} que aparentemente deberia " - f"efectuar el {reservation.agency_id if reservation.folio_id.invoice_to_agency else reservation.partner_id.name}. Puedes registrar el pago desde aquí.", + "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í.", + "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'])," - # "('checkout', '=', datetime.today())," + f"('checkout', '=', '{today}')," "]", "text": "Reserva lista para el checkout, marca la salida directamente desde aquí.", - "priority": 10, + "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"] - taoday = datetime.today() for state in sorted_wizard_states: - domain = expression.AND([ [("id", "=", reservation_id)], safe_eval(state["domain"]) ]) - if self.env["pms.reservation"].search_count(domain): - print(state) + 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='', + ) + From 7b02ba185798e0c0f0b12b1149ab93bd07f84b68 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 2 Feb 2024 17:30:59 +0100 Subject: [PATCH 487/547] [IMP]14.0-pms_api_rest: added blocked field in reservation datamodel --- pms_api_rest/datamodels/pms_reservation.py | 2 ++ pms_api_rest/services/pms_folio_service.py | 2 ++ pms_api_rest/services/pms_reservation_service.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 9bf4da99bd..964e7c2414 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -30,6 +30,7 @@ class PmsReservationShortInfo(Datamodel): 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) class PmsReservationInfo(Datamodel): @@ -92,6 +93,7 @@ class PmsReservationInfo(Datamodel): nights = fields.Integer(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) # TODO: Refact # messages = fields.List(fields.Dict(required=False, allow_none=True)) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index b913cbbf5b..6999fd9f66 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -308,6 +308,7 @@ def get_folios(self, folio_search_param): line.is_reselling for line in reservation.reservation_line_ids ), + "isBlocked": reservation.blocked } ) result_folios.append( @@ -559,6 +560,7 @@ def get_folio_reservations(self, folio_id): else 0, toAssign=reservation.to_assign, overbooking=reservation.overbooking, + isBlocked=reservation.blocked ) ) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 52b130283b..321be80f76 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -133,7 +133,7 @@ def get_reservation(self, reservation_id, pms_search_param): isReselling=any( line.is_reselling for line in reservation.reservation_line_ids ), - + isBlocked=reservation.blocked, ) return res From f8659c073d18c68a0abc83a60642ede4ed863be0 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 28 Dec 2023 19:49:36 +0100 Subject: [PATCH 488/547] [ADD]14.0-pms_ocr_regula: module to connect the OCR regula with the pms --- pms_api_rest/datamodels/__init__.py | 1 + pms_api_rest/datamodels/pms_ocr.py | 26 ++ pms_ocr_regula/README.rst | 81 ++++ pms_ocr_regula/__init__.py | 3 + pms_ocr_regula/__manifest__.py | 20 + pms_ocr_regula/data/pms_ocr_regula_data.xml | 11 + pms_ocr_regula/datamodels/__init__.py | 1 + pms_ocr_regula/datamodels/pms_property.py | 8 + pms_ocr_regula/models/__init__.py | 1 + pms_ocr_regula/models/pms_property.py | 9 + pms_ocr_regula/readme/CONTRIBUTORS.rst | 1 + pms_ocr_regula/readme/DESCRIPTION.rst | 1 + pms_ocr_regula/readme/USAGE.rst | 1 + pms_ocr_regula/services/__init__.py | 2 + .../services/ocr_document_service.py | 356 +++++++++++++++ .../services/pms_property_service.py | 26 ++ pms_ocr_regula/static/description/index.html | 426 ++++++++++++++++++ pms_ocr_regula/views/pms_property_views.xml | 14 + requirements.txt | 1 + .../pms_ocr_regula/odoo/addons/pms_ocr_regula | 1 + setup/pms_ocr_regula/setup.py | 6 + 21 files changed, 996 insertions(+) create mode 100644 pms_api_rest/datamodels/pms_ocr.py create mode 100644 pms_ocr_regula/README.rst create mode 100644 pms_ocr_regula/__init__.py create mode 100644 pms_ocr_regula/__manifest__.py create mode 100644 pms_ocr_regula/data/pms_ocr_regula_data.xml create mode 100644 pms_ocr_regula/datamodels/__init__.py create mode 100644 pms_ocr_regula/datamodels/pms_property.py create mode 100644 pms_ocr_regula/models/__init__.py create mode 100644 pms_ocr_regula/models/pms_property.py create mode 100644 pms_ocr_regula/readme/CONTRIBUTORS.rst create mode 100644 pms_ocr_regula/readme/DESCRIPTION.rst create mode 100644 pms_ocr_regula/readme/USAGE.rst create mode 100644 pms_ocr_regula/services/__init__.py create mode 100644 pms_ocr_regula/services/ocr_document_service.py create mode 100644 pms_ocr_regula/services/pms_property_service.py create mode 100644 pms_ocr_regula/static/description/index.html create mode 100644 pms_ocr_regula/views/pms_property_views.xml create mode 120000 setup/pms_ocr_regula/odoo/addons/pms_ocr_regula create mode 100644 setup/pms_ocr_regula/setup.py diff --git a/pms_api_rest/datamodels/__init__.py b/pms_api_rest/datamodels/__init__.py index df2ed820ab..ce0f40d890 100644 --- a/pms_api_rest/datamodels/__init__.py +++ b/pms_api_rest/datamodels/__init__.py @@ -65,3 +65,4 @@ from . import pms_wizard_state +from . import pms_ocr diff --git a/pms_api_rest/datamodels/pms_ocr.py b/pms_api_rest/datamodels/pms_ocr.py new file mode 100644 index 0000000000..205e6dbb22 --- /dev/null +++ b/pms_api_rest/datamodels/pms_ocr.py @@ -0,0 +1,26 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsOcrInput(Datamodel): + _name = "pms.ocr.input" + imageBase64 = fields.String(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) diff --git a/pms_ocr_regula/README.rst b/pms_ocr_regula/README.rst new file mode 100644 index 0000000000..7f2338b2ea --- /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:b34369f690039d9865de6496bc7fd3d815f16fb385b83e1e7d3db0e35ebabeb7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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 regula and url parameters of the OCR service and activate the is_used_regula field 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..c82439595a --- /dev/null +++ b/pms_ocr_regula/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import services +from . import datamodels diff --git a/pms_ocr_regula/__manifest__.py b/pms_ocr_regula/__manifest__.py new file mode 100644 index 0000000000..6bf93f99ec --- /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": ["views/pms_property_views.xml", "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/datamodels/__init__.py b/pms_ocr_regula/datamodels/__init__.py new file mode 100644 index 0000000000..9216f6ffd4 --- /dev/null +++ b/pms_ocr_regula/datamodels/__init__.py @@ -0,0 +1 @@ +from . import pms_property diff --git a/pms_ocr_regula/datamodels/pms_property.py b/pms_ocr_regula/datamodels/pms_property.py new file mode 100644 index 0000000000..15a988cb00 --- /dev/null +++ b/pms_ocr_regula/datamodels/pms_property.py @@ -0,0 +1,8 @@ +from marshmallow import fields + +from odoo.addons.datamodel.core import Datamodel + + +class PmsPropertyInfo(Datamodel): + _inherit = "pms.property.info" + isUsedRegula = fields.Boolean(required=False, allow_none=True) 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..c5530fcae2 --- /dev/null +++ b/pms_ocr_regula/models/pms_property.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class PmsProperty(models.Model): + _inherit = "pms.property" + + is_used_regula = fields.Boolean( + string="Used regula", help="True if this property uses regula's OCR" + ) 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..d74b8cd5d8 --- /dev/null +++ b/pms_ocr_regula/readme/USAGE.rst @@ -0,0 +1 @@ +Set api key regula and url parameters of the OCR service and activate the is_used_regula field in pms_property diff --git a/pms_ocr_regula/services/__init__.py b/pms_ocr_regula/services/__init__.py new file mode 100644 index 0000000000..918dbaad52 --- /dev/null +++ b/pms_ocr_regula/services/__init__.py @@ -0,0 +1,2 @@ +from . import ocr_document_service +from . import pms_property_service diff --git a/pms_ocr_regula/services/ocr_document_service.py b/pms_ocr_regula/services/ocr_document_service.py new file mode 100644 index 0000000000..a11d488131 --- /dev/null +++ b/pms_ocr_regula/services/ocr_document_service.py @@ -0,0 +1,356 @@ +from datetime import date, datetime + +from dateutil.relativedelta import relativedelta +from regula.documentreader.webclient import ( + DocumentReaderApi, + ProcessParams, + RecognitionRequest, + Result, + Scenario, + TextFieldType, +) + +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +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_regula(self, input_param): + PmsOcrCheckinResult = self.env.datamodels["pms.ocr.checkin.result"] + pms_ocr_checkin_result = PmsOcrCheckinResult() + 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=[input_param.imageBase64] + ) + response = api.process(request) + if response.text and response.text.field_list: + # for elemento in response.text.field_list: + # print("campo: ", elemento.field_name) + # print("valor: ", elemento.value) + # print('-') + 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), + ) + 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/services/pms_property_service.py b/pms_ocr_regula/services/pms_property_service.py new file mode 100644 index 0000000000..1e633dce89 --- /dev/null +++ b/pms_ocr_regula/services/pms_property_service.py @@ -0,0 +1,26 @@ +from odoo.addons.base_rest import restapi +from odoo.addons.base_rest_datamodel.restapi import Datamodel +from odoo.addons.component.core import Component + + +class PmsPropertyService(Component): + _inherit = "pms.property.service" + + @restapi.method( + [ + ( + [ + "/", + ], + "GET", + ) + ], + output_param=Datamodel("pms.property.info", is_list=True), + auth="jwt_api_pms", + ) + def get_properties(self): + result_properties = super(PmsPropertyService, self).get_properties() + for prop_info in result_properties: + pms_property = self.env["pms.property"].browse(prop_info.id) + prop_info.isUsedRegula = pms_property.is_used_regula + return result_properties diff --git a/pms_ocr_regula/static/description/index.html b/pms_ocr_regula/static/description/index.html new file mode 100644 index 0000000000..77909fefbf --- /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 regula and url parameters of the OCR service and activate the is_used_regula field 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_regula/views/pms_property_views.xml b/pms_ocr_regula/views/pms_property_views.xml new file mode 100644 index 0000000000..b7776c60b2 --- /dev/null +++ b/pms_ocr_regula/views/pms_property_views.xml @@ -0,0 +1,14 @@ + + + + pms.property + + + + + + + + + + diff --git a/requirements.txt b/requirements.txt index 093f58f35f..aa2e5d60ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,6 @@ jose jwt marshmallow pycountry +regula.documentreader.webclient simplejson xlrd 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, +) From 4fdc315f8c4c54c365f7b6eb679639149ba2e8e1 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 1 Feb 2024 18:15:15 +0100 Subject: [PATCH 489/547] [IMP]14.0-pms_api_rest: added document fields to get_partners service --- pms_api_rest/services/pms_partner_service.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 08cbe5b7e3..9c1ffa7b71 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -196,6 +196,19 @@ def get_partners(self, pms_partner_search_params): .filtered(lambda x: x.checkout) .mapped("checkout") ) + doc_number = False + document_number = False + document_expedition_date = False + document_type = False + if partner.id_numbers: + doc_number = partner.id_numbers[0] + if doc_number: + if doc_number.name: + document_number = doc_number.name + if doc_number.category_id: + document_type = doc_number.category_id.id + if doc_number.valid_from: + document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") result_partners.append( PmsPartnerInfo( id=partner.id, @@ -242,6 +255,8 @@ def get_partners(self, pms_partner_search_params): 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, vatNumber=partner.vat if partner.vat else partner.aeat_identification @@ -252,6 +267,9 @@ def get_partners(self, pms_partner_search_params): 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, From d2af0d530a6b706c4305113230d6384203ecca1d Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 15 Feb 2024 11:30:11 +0000 Subject: [PATCH 490/547] [FIX] pms-api-rest: list folios including out now --- pms_api_rest/services/pms_folio_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 6999fd9f66..d9a2e66bf1 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -255,7 +255,7 @@ def get_folios(self, folio_search_param): ) PmsFolioShortInfo = self.env.datamodels["pms.folio.short.info"] for folio in self.env["pms.folio"].search( - [("id", "in", reservations_result), ("reservation_type", "!=", "out")], + [("id", "in", reservations_result)], order=order_field, limit=folio_search_param.limit, offset=folio_search_param.offset, From 4fe937a38c0ffe0c5efbd8ae663a1a0ffa6a4798 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 15 Feb 2024 14:46:27 +0000 Subject: [PATCH 491/547] [IMP] pms-api-rest: add reservation type to folio/x/reservations service --- pms_api_rest/datamodels/pms_reservation.py | 2 ++ pms_api_rest/services/pms_folio_service.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 964e7c2414..3e60a8e3e6 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -31,6 +31,8 @@ class PmsReservationShortInfo(Datamodel): 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) + class PmsReservationInfo(Datamodel): diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d9a2e66bf1..3c6b3f2d18 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -560,7 +560,9 @@ def get_folio_reservations(self, folio_id): else 0, toAssign=reservation.to_assign, overbooking=reservation.overbooking, - isBlocked=reservation.blocked + isBlocked=reservation.blocked, + reservationType=reservation.reservation_type, + ) ) From e83842924d16956028fbeefc73759baba34bf049 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 14 Feb 2024 21:40:25 +0100 Subject: [PATCH 492/547] [IMP]pms_api_rest: added document support number to get_partners service --- pms_api_rest/services/pms_partner_service.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 9c1ffa7b71..8e10f4034f 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -200,6 +200,7 @@ def get_partners(self, pms_partner_search_params): document_number = False document_expedition_date = False document_type = False + document_support_number = False if partner.id_numbers: doc_number = partner.id_numbers[0] if doc_number: @@ -209,6 +210,8 @@ def get_partners(self, pms_partner_search_params): document_type = doc_number.category_id.id if doc_number.valid_from: document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") + if doc_number.support_number: + document_support_number = doc_number.support_number result_partners.append( PmsPartnerInfo( id=partner.id, @@ -257,6 +260,7 @@ def get_partners(self, pms_partner_search_params): 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, vatNumber=partner.vat if partner.vat else partner.aeat_identification From 13df6a8ec549f398de3bf71e34b7e3c50ece0955 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 22 Feb 2024 09:06:08 +0000 Subject: [PATCH 493/547] [FIX] pms-api-rest: fix dashboard state rooms service --- .../services/pms_dashboard_service.py | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/pms_api_rest/services/pms_dashboard_service.py b/pms_api_rest/services/pms_dashboard_service.py index f4ea451deb..92738956b2 100644 --- a/pms_api_rest/services/pms_dashboard_service.py +++ b/pms_api_rest/services/pms_dashboard_service.py @@ -159,13 +159,15 @@ def get_state_rooms(self, pms_dashboard_search_param): self.env.cr.execute( """ SELECT d.date, - COALESCE(rln.num_occupied_rooms, 0) AS num_occupied_rooms, - COALESCE( rlo.num_out_of_service_rooms, 0) AS num_out_of_service_rooms, - COUNT(r.id) free_rooms + 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 + 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 @@ -182,16 +184,12 @@ def get_state_rooms(self, pms_dashboard_search_param): AND l.occupies_availability AND r.reservation_type = 'out' GROUP BY date - ) rlo ON rlo.date = d.date, - pms_room r - WHERE r.pms_property_id = %s - AND r.id NOT IN (SELECT room_id - FROM pms_reservation_line l - WHERE l.date = d.date - AND l.occupies_availability - AND l.pms_property_id = %s - ) - GROUP BY d.date, num_occupied_rooms, num_out_of_service_rooms + ) 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 """, ( @@ -200,7 +198,6 @@ def get_state_rooms(self, pms_dashboard_search_param): pms_dashboard_search_param.pmsPropertyId, pms_dashboard_search_param.pmsPropertyId, pms_dashboard_search_param.pmsPropertyId, - pms_dashboard_search_param.pmsPropertyId, ), ) From a6f22282cf4e5eefa2fd13daffb1403e7f45aad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 21 Feb 2024 09:43:08 +0100 Subject: [PATCH 494/547] [IMP]pms_api_rest: PUT external app search folio by ilike external_reference --- pms_api_rest/services/pms_folio_service.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 3c6b3f2d18..f335d22abd 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -308,7 +308,7 @@ def get_folios(self, folio_search_param): line.is_reselling for line in reservation.reservation_line_ids ), - "isBlocked": reservation.blocked + "isBlocked": reservation.blocked, } ) result_folios.append( @@ -562,7 +562,6 @@ def get_folio_reservations(self, folio_id): overbooking=reservation.overbooking, isBlocked=reservation.blocked, reservationType=reservation.reservation_type, - ) ) @@ -1508,7 +1507,7 @@ def get_board_service_room_type_id( def update_put_external_folio(self, external_reference, pms_folio_info): folio = self.env["pms.folio"].search( [ - ("external_reference", "=", external_reference), + ("external_reference", "ilike", external_reference), ("pms_property_id", "=", pms_folio_info.pmsPropertyId), ] ) From a4a79638ed9c61167138bc7173cb5d32443c586e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 1 Jan 2024 19:01:41 +0100 Subject: [PATCH 495/547] [IMP]pms_api_rest: force update avail in channel reservations changes --- pms_api_rest/services/pms_folio_service.py | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index f335d22abd..6c4e61da7e 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -745,6 +745,16 @@ def create_folio(self, pms_folio_info): "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, + ) return folio.id def compute_transactions(self, folio, transactions): @@ -1597,6 +1607,16 @@ def update_folio_values(self, folio, pms_folio_info): ).write(folio_vals) 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 wrapper_reservations(self, folio, info_reservations): """ @@ -1683,3 +1703,24 @@ def wrapper_reservation_services(self, info_reservations): ) ) 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. + """ + if not room_type_ids: + return False + for room_type_id in room_type_ids: + pms_property_id = self.env["pms.property"].browse(pms_property_id) + self.env["pms.property"].neobookings_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_id.pms_property_code], + ) From 38f7b2b601bb6f47afe5e34fdd9378b93db1316d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 31 Dec 2023 12:01:53 +0100 Subject: [PATCH 496/547] [ADD]pms_api_rest: OTA API configurations --- pms_api_rest/__manifest__.py | 1 + pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/ota_property_settings.py | 34 +++++++++++ pms_api_rest/models/pms_property.py | 7 +++ pms_api_rest/models/res_users.py | 11 ++++ pms_api_rest/security/ir.model.access.csv | 3 + pms_api_rest/services/pms_folio_service.py | 59 +++++++++++++++++--- pms_api_rest/views/pms_property_views.xml | 16 ++++++ pms_api_rest/views/res_users_views.xml | 12 ++++ 9 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 pms_api_rest/models/ota_property_settings.py create mode 100644 pms_api_rest/security/ir.model.access.csv diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index fdde38e461..1c48864e8c 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -21,6 +21,7 @@ "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", diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 09f08a5897..06aad42855 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -5,3 +5,4 @@ from . import pms_room_type_class from . import account_bank_statement from . import product_template +from . import ota_property_settings 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..0298b8bbc3 --- /dev/null +++ b/pms_api_rest/models/ota_property_settings.py @@ -0,0 +1,34 @@ +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. + """, + ) diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index c15c95dd1a..3ef24dd618 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -93,3 +93,10 @@ class PmsProperty(models.Model): 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", + ) diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index c394fb4185..0708fb705e 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -22,6 +22,17 @@ class ResUsers(models.Model): readonly=False, ) + pms_api_client = fields.Boolean( + string="PMS API Client", + help="PMS API Client", + ) + + pms_api_payment_journal_id = fields.Many2one( + string="Payment Journal", + help="Payment Journal", + comodel_name="account.journal", + ) + def _get_default_avail_rule_fields(self): default_avail_rule_fields = self.env["ir.model.fields"].search( [ 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..f818625d3d --- /dev/null +++ b/pms_api_rest/security/ir.model.access.csv @@ -0,0 +1,3 @@ +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 diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 6c4e61da7e..508a7c923e 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -772,12 +772,7 @@ def compute_transactions(self, folio, transactions): ("ref", "ilike", transaction.reference), ] ): - # TODO: Move this to the user API payment configuration - journal = ( - self.env["channel.wubook.backend"] - .search([("pms_property_id", "=", folio.pms_property_id.id)]) - .wubook_journal_id - ) + journal = transaction.journalId if transaction.transactionType == "inbound": folio.do_payment( journal, @@ -1438,7 +1433,7 @@ def get_api_client_type(self): # - Channel Manager # - Booking Engine # - ... - if "neobookings" in self.env.user.login: + if self.env.user.pms_api_client: return "external_app" return "internal_app" @@ -1605,6 +1600,8 @@ def update_folio_values(self, folio, pms_folio_info): skip_compute_service_ids=True, force_overbooking=True if call_type == "external_app" else False, ).write(folio_vals) + # Compute OTA transactions + pms_folio_info.transactions = self.normalize_payments_structure(pms_folio_info) if pms_folio_info.transactions: self.compute_transactions(folio, pms_folio_info.transactions) # Force update availability @@ -1618,6 +1615,54 @@ def update_folio_values(self, folio, pms_folio_info): date_to=date_to, ) + def normalize_payments_structure(self, pms_folio_info): + """ + This method use the OTA payment structure to normalize the structure + and incorporate them in the transactions datamodel param + """ + if pms_folio_info.transactions: + 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), + ] + ) + 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_info.transactions = [ + pmsTransactionInfo( + journalId=journal.id, + transactionType="inbound", + amount=pms_folio_info.totalPrice, + date=fields.Date.today().strftime("%Y-%m-%d"), + reference=pms_folio_info.externalReference, + ) + ] + def wrapper_reservations(self, folio, info_reservations): """ This method is used to create or update the reservations in folio diff --git a/pms_api_rest/views/pms_property_views.xml b/pms_api_rest/views/pms_property_views.xml index 39b4a2bfca..03a52368f4 100644 --- a/pms_api_rest/views/pms_property_views.xml +++ b/pms_api_rest/views/pms_property_views.xml @@ -90,6 +90,22 @@
+ + + + + + + + + + + + +
diff --git a/pms_api_rest/views/res_users_views.xml b/pms_api_rest/views/res_users_views.xml index 0049c49065..7233cd26ab 100644 --- a/pms_api_rest/views/res_users_views.xml +++ b/pms_api_rest/views/res_users_views.xml @@ -17,6 +17,18 @@ options="{'no_create': True}" domain="['&',('model_id', '=', 'pms.availability.plan.rule'), ('name', 'in', ('min_stay', 'max_stay', 'quota', 'max_stay_arrival', 'closed_arrival', 'closed', 'closed_departure', 'min_stay_arrival', 'max_avail'))]" /> + + From 510de761490d965282064b848926dd009de4f7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 25 Feb 2024 11:42:43 +0100 Subject: [PATCH 497/547] [IMP]pms_api_rest: Improvement in the handling of requests from external apps through the API --- pms_api_rest/models/res_users.py | 6 -- .../services/pms_board_service_service.py | 20 ++++++ pms_api_rest/services/pms_folio_service.py | 64 ++++++++++--------- pms_api_rest/views/res_users_views.xml | 7 -- 4 files changed, 55 insertions(+), 42 deletions(-) diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index 0708fb705e..9c173007bd 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -27,12 +27,6 @@ class ResUsers(models.Model): help="PMS API Client", ) - pms_api_payment_journal_id = fields.Many2one( - string="Payment Journal", - help="Payment Journal", - comodel_name="account.journal", - ) - def _get_default_avail_rule_fields(self): default_avail_rule_fields = self.env["ir.model.fields"].search( [ diff --git a/pms_api_rest/services/pms_board_service_service.py b/pms_api_rest/services/pms_board_service_service.py index 9b087f4fe5..58528bd907 100644 --- a/pms_api_rest/services/pms_board_service_service.py +++ b/pms_api_rest/services/pms_board_service_service.py @@ -26,6 +26,7 @@ class PmsBoardServiceService(Component): 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)) @@ -57,6 +58,25 @@ def get_board_services(self, board_services_search_param): ), ) ) + 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, + productIds=[], + ) + ) return result_board_services @restapi.method( diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 508a7c923e..5db13fe9c9 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -581,7 +581,7 @@ def get_folio_reservations(self, folio_id): ) # flake8:noqa=C901 def create_folio(self, pms_folio_info): - call_type = self.get_api_client_type() + external_app = self.env.user.pms_api_client if pms_folio_info.reservationType == "out": vals = { "pms_property_id": pms_folio_info.pmsPropertyId, @@ -652,6 +652,13 @@ def create_folio(self, pms_folio_info): } 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"] + ) + board_day_price = board.amount * reservation.adults for reservationLine in reservation.reservationLines: vals_lines.append( ( @@ -659,7 +666,7 @@ def create_folio(self, pms_folio_info): 0, { "date": reservationLine.date, - "price": reservationLine.price, + "price": reservationLine.price - board_day_price, "discount": reservationLine.discount, }, ) @@ -672,10 +679,8 @@ def create_folio(self, pms_folio_info): reservation_record = ( self.env["pms.reservation"] .with_context( - skip_compute_service_ids=False - if call_type == "external_app" - else True, - force_overbooking=True if call_type == "external_app" else False, + skip_compute_service_ids=False if external_app else True, + force_overbooking=True if external_app else False, ) .create(vals) ) @@ -1423,29 +1428,16 @@ def parse_message_body(self, message): ) return message_body - def get_api_client_type(self): - """ - Returns the type of the call: - - Internal APP: The call is made from the internal vue app - - External APP: The call is made from an external app - """ - # TODO: Set the new roles in API Key users: - # - Channel Manager - # - Booking Engine - # - ... - if self.env.user.pms_api_client: - return "external_app" - return "internal_app" - 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 self.get_api_client_type() == "external_app": + if not agency_id and external_app: # TODO change by configuration user api in the future return ( self.env["pms.sale.channel"] @@ -1464,7 +1456,8 @@ def get_language(self, lang_code): """ Returns the language for the given language code """ - if self.get_api_client_type() == "internal_app": + 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 @@ -1478,7 +1471,8 @@ def get_board_service_room_type_id( """ board_service = self.env["pms.board.service"].browse(board_service_id) room_type = self.env["pms.room.type"].browse(room_type_id) - if self.get_api_client_type() == "internal_app": + external_app = self.env.user.pms_api_client + if not external_app: return board_service_id if board_service and room_type: return ( @@ -1541,7 +1535,7 @@ def update_put_folio(self, folio_id, pms_folio_info): return folio.id def update_folio_values(self, folio, pms_folio_info): - call_type = self.get_api_client_type() + external_app = self.env.user.pms_api_client folio_vals = {} if pms_folio_info.state == "cancel": folio.action_cancel() @@ -1597,8 +1591,8 @@ def update_folio_values(self, folio, pms_folio_info): lambda r: r.state != "cancel" ).with_context(modified=True, force_write_blocked=True).action_cancel() folio.with_context( - skip_compute_service_ids=True, - force_overbooking=True if call_type == "external_app" else False, + skip_compute_service_ids=False if external_app else True, + force_overbooking=True if external_app else False, ).write(folio_vals) # Compute OTA transactions pms_folio_info.transactions = self.normalize_payments_structure(pms_folio_info) @@ -1621,6 +1615,9 @@ def normalize_payments_structure(self, pms_folio_info): 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( @@ -1671,6 +1668,7 @@ def wrapper_reservations(self, folio, info_reservations): 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 = [] for info_reservation in info_reservations: vals = {} @@ -1700,8 +1698,16 @@ def wrapper_reservations(self, folio, info_reservations): if info_reservation.children: vals.update({"children": info_reservation.children}) 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"] + ) + board_day_price = board.amount * info_reservation.adults reservation_lines_cmds = self.wrapper_reservation_lines( - info_reservation + reservation=info_reservation, + board_day_price=board_day_price, ) if reservation_lines_cmds: vals.update({"reservation_line_ids": reservation_lines_cmds}) @@ -1717,7 +1723,7 @@ def wrapper_reservations(self, folio, info_reservations): cmds.append((0, False, vals)) return cmds - def wrapper_reservation_lines(self, reservation): + def wrapper_reservation_lines(self, reservation, board_day_price=0): cmds = [] for line in reservation.reservationLines: cmds.append( @@ -1726,7 +1732,7 @@ def wrapper_reservation_lines(self, reservation): False, { "date": line.date, - "price": line.price, + "price": line.price - board_day_price, "discount": line.discount or 0, }, ) diff --git a/pms_api_rest/views/res_users_views.xml b/pms_api_rest/views/res_users_views.xml index 7233cd26ab..81176b78bf 100644 --- a/pms_api_rest/views/res_users_views.xml +++ b/pms_api_rest/views/res_users_views.xml @@ -22,13 +22,6 @@ string="PMS API Client" help="This user is used to PMS API's client (channel managers, precheckin apps, booking engines, etc.)" /> - From b643577faddfbd2e7830f93a119234c0a631e88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 25 Feb 2024 11:56:01 +0100 Subject: [PATCH 498/547] [IMP]pms_api_rest: Add property push methods to pms api clients --- pms_api_rest/models/pms_property.py | 527 +++++++++++++++++++++++++++- 1 file changed, 526 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index 3ef24dd618..ec4df7f74a 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -1,4 +1,14 @@ -from odoo import fields, models +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): @@ -100,3 +110,518 @@ class PmsProperty(models.Model): comodel_name="ota.property.settings", inverse_name="pms_property_id", ) + + # PUSH API NOTIFICATIONS + def get_payload_avail(self, avails, client): + self.ensure_one() + endpoint = client.url_endpoint_avail + pms_property_id = self.id + avails_dict = {"pmsPropertyId": pms_property_id, "avails": []} + room_type_ids = avails.mapped("room_type_id.id") + plan_avail = client.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) + ] + plan_avail = client.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 + } + ] + """ + 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", "=", self.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 ( + 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 + pms_property = self.env["pms.property"].browse(pms_property_id) + pricelist = client.main_pricelist_id + product_context = dict( + self.env.context, + date=datetime.datetime.today().date(), + pricelist=3, # self.get_default_pricelist(), + 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)]) + _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(): + raise ValidationError(_("Invalid date from")) + if isinstance(date_to, str): + date_to = datetime.datetime.strptime(date_to, "%Y-%m-%d").date() + if date_to <= date_from: + raise ValidationError(_("Invalid date to")) + 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: + 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 client.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_avail + 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() From c395c95f495ece90ad0fe7c6deb232a6bfa26abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 26 Feb 2024 09:51:14 +0100 Subject: [PATCH 499/547] [ADD]pms_api_rest: PMS API Client conexion data --- pms_api_rest/models/ota_property_settings.py | 15 ++++++ pms_api_rest/models/pms_property.py | 51 +++++++++++++++++--- pms_api_rest/models/res_users.py | 17 ++++++- pms_api_rest/services/pms_folio_service.py | 14 ++++-- pms_api_rest/views/pms_property_views.xml | 10 ++++ pms_api_rest/views/res_users_views.xml | 26 ++++++++++ 6 files changed, 121 insertions(+), 12 deletions(-) diff --git a/pms_api_rest/models/ota_property_settings.py b/pms_api_rest/models/ota_property_settings.py index 0298b8bbc3..1e79642bd8 100644 --- a/pms_api_rest/models/ota_property_settings.py +++ b/pms_api_rest/models/ota_property_settings.py @@ -32,3 +32,18 @@ class OtaPropertySettings(models.Model): 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_property.py b/pms_api_rest/models/pms_property.py index ec4df7f74a..31afd5e25c 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -114,11 +114,17 @@ class PmsProperty(models.Model): # PUSH API NOTIFICATIONS def get_payload_avail(self, avails, client): self.ensure_one() - endpoint = client.url_endpoint_avail + 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") - plan_avail = client.main_avail_plan_id + property_client_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", self.id), + ("agency_id", "=", client.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), @@ -292,7 +298,13 @@ def generate_availability_json( date_from + datetime.timedelta(days=x) for x in range((date_to - date_from).days + 1) ] - plan_avail = client.main_avail_plan_id + property_client_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", pms_property_id), + ("agency_id", "=", client.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: @@ -386,13 +398,23 @@ def generate_restrictions_json( } ] """ + property_client_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", pms_property_id), + ("agency_id", "=", client.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", "=", self.main_avail_plan_id.id), + ( + "availability_plan_id", + "=", + property_client_conf.main_avail_plan_id.id, + ), ], order="date", ) @@ -463,8 +485,14 @@ def generate_prices_json( 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.id), + ] + ) pms_property = self.env["pms.property"].browse(pms_property_id) - pricelist = client.main_pricelist_id + pricelist = property_client_conf.main_pricelist_id product_context = dict( self.env.context, date=datetime.datetime.today().date(), @@ -544,6 +572,12 @@ def pms_api_push_batch( clients = client else: clients = self.env["res.users"].search([("pms_api_client", "=", True)]) + property_client_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "in", clients.pms_property_ids.ids), + ("agency_id", "in", clients.ids), + ] + ) _logger.info("PMS API push batch") if isinstance(date_from, str): date_from = datetime.datetime.strptime(date_from, "%Y-%m-%d").date() @@ -571,7 +605,10 @@ def pms_api_push_batch( else self.env["pms.room"] .search([("pms_property_id", "=", pms_property_id)]) .mapped("room_type_id") - .filtered(lambda r: r.id not in client.excluded_room_type_ids.ids) + .filtered( + lambda r: r.id + not in property_client_conf.excluded_room_type_ids.ids + ) .ids ) payload = { @@ -580,7 +617,7 @@ def pms_api_push_batch( data = [] for room_type_id in room_type_ids: if call_type == "availability": - endpoint = client.url_endpoint_avail + endpoint = client.url_endpoint_availability data.extend( pms_property.generate_availability_json( date_from=date_from, diff --git a/pms_api_rest/models/res_users.py b/pms_api_rest/models/res_users.py index 9c173007bd..85e1745636 100644 --- a/pms_api_rest/models/res_users.py +++ b/pms_api_rest/models/res_users.py @@ -21,11 +21,26 @@ class ResUsers(models.Model): 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( diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 5db13fe9c9..fb97dca117 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1764,14 +1764,20 @@ def force_api_update_avail( It is used to override potential availability changes on the channel made unilaterally, for example, upon entering or canceling a reservation. """ - if not room_type_ids: + api_clients = self.env["res.users"].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_id = self.env["pms.property"].browse(pms_property_id) - self.env["pms.property"].neobookings_push_batch( + pms_property = self.env["pms.property"].browse(pms_property_id) + self.env["pms.property"].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_id.pms_property_code], + pms_property_codes=[pms_property.pms_property_code], ) diff --git a/pms_api_rest/views/pms_property_views.xml b/pms_api_rest/views/pms_property_views.xml index 03a52368f4..e0b5286157 100644 --- a/pms_api_rest/views/pms_property_views.xml +++ b/pms_api_rest/views/pms_property_views.xml @@ -102,6 +102,16 @@ options="{'no_create': True}" /> + + + diff --git a/pms_api_rest/views/res_users_views.xml b/pms_api_rest/views/res_users_views.xml index 81176b78bf..c639349d58 100644 --- a/pms_api_rest/views/res_users_views.xml +++ b/pms_api_rest/views/res_users_views.xml @@ -17,11 +17,37 @@ options="{'no_create': True}" domain="['&',('model_id', '=', 'pms.availability.plan.rule'), ('name', 'in', ('min_stay', 'max_stay', 'quota', 'max_stay_arrival', 'closed_arrival', 'closed', 'closed_departure', 'min_stay_arrival', 'max_avail'))]" /> + + + + + + From 243eae237ba20e06edef028bc7aca6c40caf7c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 27 Feb 2024 12:55:35 +0100 Subject: [PATCH 500/547] [ADD]pms_api_rest: logs api client model --- pms_api_rest/__manifest__.py | 1 + pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/pms_api_log.py | 74 ++++ pms_api_rest/security/ir.model.access.csv | 1 + pms_api_rest/services/pms_folio_service.py | 418 ++++++++++++--------- pms_api_rest/views/pms_api_log_views.xml | 75 ++++ 6 files changed, 398 insertions(+), 172 deletions(-) create mode 100644 pms_api_rest/models/pms_api_log.py create mode 100644 pms_api_rest/views/pms_api_log_views.xml diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 1c48864e8c..80661e978d 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -29,6 +29,7 @@ "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", diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 06aad42855..d57002e00a 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -6,3 +6,4 @@ from . import account_bank_statement from . import product_template from . import ota_property_settings +from . import pms_api_log 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..2509ee5511 --- /dev/null +++ b/pms_api_rest/models/pms_api_log.py @@ -0,0 +1,74 @@ +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", + ) diff --git a/pms_api_rest/security/ir.model.access.csv b/pms_api_rest/security/ir.model.access.csv index f818625d3d..bb8bad458f 100644 --- a/pms_api_rest/security/ir.model.access.csv +++ b/pms_api_rest/security/ir.model.access.csv @@ -1,3 +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/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index fb97dca117..8493fd2de9 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -582,185 +582,223 @@ def get_folio_reservations(self, folio_id): # flake8:noqa=C901 def create_folio(self, pms_folio_info): external_app = self.env.user.pms_api_client - 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, - } - ) + 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: - 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 = { + "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( { - "email": pms_folio_info.partnerEmail, + "partner_id": pms_folio_info.partnerId, } ) - 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, - } - 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"] - ) - board_day_price = board.amount * reservation.adults - for reservationLine in reservation.reservationLines: - vals_lines.append( - ( - 0, - 0, + else: + if pms_folio_info.partnerName: + vals.update( { - "date": reservationLine.date, - "price": reservationLine.price - board_day_price, - "discount": reservationLine.discount, - }, + "partner_name": pms_folio_info.partnerName, + } ) - ) - vals["reservation_line_ids"] = vals_lines - else: - vals["checkin"] = reservation.checkin - vals["checkout"] = reservation.checkout + 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, + } + 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"] + ) + board_day_price = board.amount * reservation.adults + 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, + 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, + ) + .create(vals) ) - .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, + 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( { - "date": line.date, - "price_unit": line.priceUnit, - "discount": line.discount or 0, - "day_qty": line.quantity, - }, + "product_qty": service.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 - )._compute_board_service_room_id() - 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") + 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 + )._compute_board_service_room_id() + 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": pms_folio_info, + "response": folio.id, + "status": "success", + "request_date": fields.Datetime.now(), + "method": "POST", + "endpoint": "/folios", + } ) - 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, - ) - return folio.id + 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": pms_folio_info, + "response": e, + "status": "error", + "request_date": fields.Datetime.now(), + "method": "POST", + "endpoint": "/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: @@ -1528,11 +1566,47 @@ def update_put_external_folio(self, external_reference, pms_folio_info): auth="jwt_api_pms", ) def update_put_folio(self, folio_id, pms_folio_info): - folio = self.env["pms.folio"].browse(folio_id) - if not folio: - raise MissingError(_("Folio not found")) - self.update_folio_values(folio, pms_folio_info) - return folio.id + external_app = self.env.user.pms_api_client + 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"].create( + { + "pms_property_id": pms_folio_info.pmsPropertyId, + "client_id": self.env.user.id, + "request": pms_folio_info, + "response": folio.id, + "status": "success", + "request_date": fields.Datetime.now(), + "method": "PUT", + "endpoint": "/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": pms_folio_info, + "response": e, + "status": "error", + "request_date": fields.Datetime.now(), + "method": "PUT", + "endpoint": "/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 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..75d83467de --- /dev/null +++ b/pms_api_rest/views/pms_api_log_views.xml @@ -0,0 +1,75 @@ + + + + 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 + + + + +
From 22daabbf658023b0103a062d82cce97dc13f5bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 2 Mar 2024 18:09:52 +0100 Subject: [PATCH 501/547] [IMP]pms_api_rest: sql report service sql update --- pms_api_rest/data/sql_reports.xml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pms_api_rest/data/sql_reports.xml b/pms_api_rest/data/sql_reports.xml index 391095cc8d..d045c9876b 100644 --- a/pms_api_rest/data/sql_reports.xml +++ b/pms_api_rest/data/sql_reports.xml @@ -110,23 +110,27 @@ line.day_qty as "Units", reservation.adults as "Room Adults", reservation.children as "Room Childrens", - line.is_board_service as "Board Service" + 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 + ON line.product_id = product.id LEFT JOIN product_template product_tmpl - ON product.product_tmpl_id = product_tmpl.id + ON product.product_tmpl_id = product_tmpl.id LEFT JOIN pms_reservation reservation - ON line.reservation_id = reservation.id + ON line.reservation_id = reservation.id LEFT JOIN pms_checkin_partner room_host - ON room_host.reservation_id = reservation.id + 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) - GROUP BY - line.id, product_tmpl.name, reservation.name, reservation.rooms, reservation.adults, reservation.children; + 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
From e727d410763f95a1bf246970cfb62c3f16918a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 2 Mar 2024 18:11:49 +0100 Subject: [PATCH 502/547] [ADD]pms_api_rest: sale_channel_service isOnLine search param --- pms_api_rest/datamodels/pms_sale_channel.py | 2 ++ pms_api_rest/models/pms_property.py | 23 ++++++++++--------- .../services/pms_sale_channel_service.py | 3 +++ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/datamodels/pms_sale_channel.py b/pms_api_rest/datamodels/pms_sale_channel.py index 730bb8037d..10eee3d098 100644 --- a/pms_api_rest/datamodels/pms_sale_channel.py +++ b/pms_api_rest/datamodels/pms_sale_channel.py @@ -6,6 +6,7 @@ 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): @@ -14,3 +15,4 @@ class PmsSaleChannelInfo(Datamodel): 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/models/pms_property.py b/pms_api_rest/models/pms_property.py index 31afd5e25c..45d558330e 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -121,7 +121,7 @@ def get_payload_avail(self, avails, client): property_client_conf = self.env["ota.property.settings"].search( [ ("pms_property_id", "=", self.id), - ("agency_id", "=", client.id), + ("agency_id", "=", client.partner_id.id), ] ) plan_avail = property_client_conf.main_avail_plan_id @@ -301,7 +301,7 @@ def generate_availability_json( property_client_conf = self.env["ota.property.settings"].search( [ ("pms_property_id", "=", pms_property_id), - ("agency_id", "=", client.id), + ("agency_id", "=", client.partner_id.id), ] ) plan_avail = property_client_conf.main_avail_plan_id @@ -401,7 +401,7 @@ def generate_restrictions_json( property_client_conf = self.env["ota.property.settings"].search( [ ("pms_property_id", "=", pms_property_id), - ("agency_id", "=", client.id), + ("agency_id", "=", client.partner_id.id), ] ) rules_records = self.env["pms.availability.plan.rule"].search( @@ -437,7 +437,8 @@ def generate_restrictions_json( current_date_from = date current_date_to = date elif ( - current_rule.min_stay == rule.min_stay + 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 @@ -488,7 +489,7 @@ def generate_prices_json( property_client_conf = self.env["ota.property.settings"].search( [ ("pms_property_id", "=", pms_property_id), - ("agency_id", "=", client.id), + ("agency_id", "=", client.partner_id.id), ] ) pms_property = self.env["pms.property"].browse(pms_property_id) @@ -572,12 +573,6 @@ def pms_api_push_batch( clients = client else: clients = self.env["res.users"].search([("pms_api_client", "=", True)]) - property_client_conf = self.env["ota.property.settings"].search( - [ - ("pms_property_id", "in", clients.pms_property_ids.ids), - ("agency_id", "in", clients.ids), - ] - ) _logger.info("PMS API push batch") if isinstance(date_from, str): date_from = datetime.datetime.strptime(date_from, "%Y-%m-%d").date() @@ -598,6 +593,12 @@ def pms_api_push_batch( ] ) for pms_property in pms_properties: + property_client_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", pms_property.id), + ("agency_id", "=", client.partner_id.id), + ] + ) pms_property_id = pms_property.id room_type_ids = ( [filter_room_type_id] diff --git a/pms_api_rest/services/pms_sale_channel_service.py b/pms_api_rest/services/pms_sale_channel_service.py index fa8dd999be..d336874ec0 100644 --- a/pms_api_rest/services/pms_sale_channel_service.py +++ b/pms_api_rest/services/pms_sale_channel_service.py @@ -51,6 +51,8 @@ def get_sale_channels(self, sale_channel_search_param): 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"] @@ -67,6 +69,7 @@ def get_sale_channels(self, sale_channel_search_param): iconUrl=url_image_pms_api_rest( "pms.sale.channel", sale_channel.id, "icon" ), + isOnLine=sale_channel.is_on_line, ) ) return result_sale_channels From cc501bce19dd48f86e2b0455bfe30cff4b25bc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 2 Mar 2024 18:13:22 +0100 Subject: [PATCH 503/547] [IMP]pms_api_rest: Rss post imageUrl in service --- pms_api_rest/services/feed_post_service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pms_api_rest/services/feed_post_service.py b/pms_api_rest/services/feed_post_service.py index 27d5660cb3..3b4c832b2b 100644 --- a/pms_api_rest/services/feed_post_service.py +++ b/pms_api_rest/services/feed_post_service.py @@ -33,8 +33,7 @@ def get_feed_posts(self): description=rss.description, publishDate=str(rss.publish_date), author=rss.author if rss.author else "", - imageUrl="https://www.roomdoo.com/wp-content" - "/uploads/2021/09/hotel-roomdoo.png", + imageUrl=rss.image_url or "", ) ) return result_rss From 651af326e8edcd1aa91368f57bec122b94e60613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 2 Mar 2024 18:15:17 +0100 Subject: [PATCH 504/547] [IMP]pms_api_rest: Improvements pms api logs and price night compute qith boardservice children amount --- pms_api_rest/services/pms_folio_service.py | 134 ++++++++++++++++++--- 1 file changed, 115 insertions(+), 19 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 8493fd2de9..42880f8b16 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -582,6 +582,7 @@ def get_folio_reservations(self, folio_id): # flake8:noqa=C901 def create_folio(self, pms_folio_info): external_app = self.env.user.pms_api_client + log_payload = pms_folio_info try: if pms_folio_info.reservationType == "out": vals = { @@ -659,7 +660,28 @@ def create_folio(self, pms_folio_info): board = self.env["pms.board.service.room.type"].browse( vals["board_service_room_id"] ) - board_day_price = board.amount * reservation.adults + 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( ( @@ -733,6 +755,9 @@ def create_folio(self, pms_folio_info): reservation_record.with_context( skip_compute_service_ids=False )._compute_board_service_room_id() + pms_folio_info.transactions = self.normalize_payments_structure( + pms_folio_info + ) 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 @@ -768,7 +793,7 @@ def create_folio(self, pms_folio_info): { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, - "request": pms_folio_info, + "request": log_payload, "response": folio.id, "status": "success", "request_date": fields.Datetime.now(), @@ -787,7 +812,7 @@ def create_folio(self, pms_folio_info): { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, - "request": pms_folio_info, + "request": log_payload, "response": e, "status": "error", "request_date": fields.Datetime.now(), @@ -815,7 +840,18 @@ def compute_transactions(self, folio, transactions): ("ref", "ilike", transaction.reference), ] ): - journal = transaction.journalId + 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, @@ -1542,16 +1578,53 @@ def get_board_service_room_type_id( auth="jwt_api_pms", ) def update_put_external_folio(self, external_reference, pms_folio_info): - 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) - return folio.id + external_app = self.env.user.pms_api_client + log_payload = pms_folio_info + 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"].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", + } + ) + 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", + } + ) + if not external_app: + raise ValidationError(_("Error updating folio from API: %s") % e) + else: + return False @restapi.method( [ @@ -1567,6 +1640,7 @@ def update_put_external_folio(self, external_reference, pms_folio_info): ) def update_put_folio(self, folio_id, pms_folio_info): external_app = self.env.user.pms_api_client + log_payload = pms_folio_info try: folio = self.env["pms.folio"].browse(folio_id) if not folio: @@ -1576,7 +1650,7 @@ def update_put_folio(self, folio_id, pms_folio_info): { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, - "request": pms_folio_info, + "request": log_payload, "response": folio.id, "status": "success", "request_date": fields.Datetime.now(), @@ -1593,9 +1667,9 @@ def update_put_folio(self, folio_id, pms_folio_info): ) self.env["pms.api.log"].sudo().create( { - "pms_property_id": pms_folio_info.pmsPropertyId, + "pms_property_id": log_payload.pmsPropertyId, "client_id": self.env.user.id, - "request": pms_folio_info, + "request": log_payload, "response": e, "status": "error", "request_date": fields.Datetime.now(), @@ -1724,7 +1798,7 @@ def normalize_payments_structure(self, pms_folio_info): ): journal = ota_conf.pms_api_payment_journal_id pmsTransactionInfo = self.env.datamodels["pms.transaction.info"] - pms_folio_info.transactions = [ + pms_folio_infotransactions = [ pmsTransactionInfo( journalId=journal.id, transactionType="inbound", @@ -1733,6 +1807,7 @@ def normalize_payments_structure(self, pms_folio_info): reference=pms_folio_info.externalReference, ) ] + return pms_folio_info.transactions def wrapper_reservations(self, folio, info_reservations): """ @@ -1778,7 +1853,28 @@ def wrapper_reservations(self, folio, info_reservations): board = self.env["pms.board.service.room.type"].browse( vals["board_service_room_id"] ) - board_day_price = board.amount * info_reservation.adults + 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, From b0a3324920722db92b41dc962bf1120c315bee50 Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 7 Mar 2024 11:32:10 +0000 Subject: [PATCH 505/547] [FIX] pms-api-rest: fix data/sql_reports.xml (<= - <=) & fix PascalCase to camelCase in datamodels/pms.sale.channel.searchParam.isOnline --- pms_api_rest/data/sql_reports.xml | 2 +- pms_api_rest/datamodels/pms_sale_channel.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/data/sql_reports.xml b/pms_api_rest/data/sql_reports.xml index d045c9876b..099b967489 100644 --- a/pms_api_rest/data/sql_reports.xml +++ b/pms_api_rest/data/sql_reports.xml @@ -123,7 +123,7 @@ 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.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 diff --git a/pms_api_rest/datamodels/pms_sale_channel.py b/pms_api_rest/datamodels/pms_sale_channel.py index 10eee3d098..6b5cc99e33 100644 --- a/pms_api_rest/datamodels/pms_sale_channel.py +++ b/pms_api_rest/datamodels/pms_sale_channel.py @@ -6,7 +6,7 @@ class PmsSaleChannelSearchParam(Datamodel): _name = "pms.sale.channel.search.param" pmsPropertyIds = fields.List(fields.Integer(), required=False) - IsOnLine = fields.Boolean(required=False, allow_none=True) + isOnLine = fields.Boolean(required=False, allow_none=True) class PmsSaleChannelInfo(Datamodel): From 480a87e90c9260c27b741ecf2f0633c0cfdd2bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 16 Mar 2024 11:14:53 +0100 Subject: [PATCH 506/547] [IMP]pms_api_rest: Improvemente Api rest logs and PUT --- pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/pms_api_log.py | 62 +++++++- pms_api_rest/models/pms_folio.py | 15 ++ pms_api_rest/services/pms_folio_service.py | 172 +++++++++++++++++---- pms_api_rest/views/pms_api_log_views.xml | 7 + 5 files changed, 225 insertions(+), 32 deletions(-) create mode 100644 pms_api_rest/models/pms_folio.py diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index d57002e00a..8a708fe07a 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -7,3 +7,4 @@ from . import product_template from . import ota_property_settings from . import pms_api_log +from . import pms_folio diff --git a/pms_api_rest/models/pms_api_log.py b/pms_api_rest/models/pms_api_log.py index 2509ee5511..9e3dcb2bff 100644 --- a/pms_api_rest/models/pms_api_log.py +++ b/pms_api_rest/models/pms_api_log.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import _, api, fields, models class PmsApiLog(models.Model): @@ -72,3 +72,63 @@ class PmsApiLog(models.Model): string="Response URL", help="Response URL", ) + model_id = fields.Many2one( + string="Model", + help="Model", + comodel_name="ir.model", + ) + + 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() + if "pms_api_log_id" in self.env[self.model_id.model]._fields: + records = self.env[self.model_id.model].search( + [("pms_api_log_id", "=", self.id)] + ) + 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 + + @api.model + def create(self, vals): + """ + set pms_api_log_id and origin_json in related records + if record_ids id present in context + """ + log_record = super().create(vals) + if self.env.context.get("record_ids"): + records = self.env[self.env.context.get("model")].browse( + self.env.context.get("record_ids") + ) + records.write( + { + "pms_api_log_id": log_record.id, + "origin_json": log_record.request, + } + ) + return log_record diff --git a/pms_api_rest/models/pms_folio.py b/pms_api_rest/models/pms_folio.py new file mode 100644 index 0000000000..498b1f3fb6 --- /dev/null +++ b/pms_api_rest/models/pms_folio.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class PmsFolio(models.Model): + _name = "pms.folio" + + pms_api_log_id = fields.Many2one( + string="PMS API Log", + help="PMS API Log", + comodel_name="pms.api.log", + ) + origin_json = fields.Text( + string="Origin JSON", + help="Origin JSON", + ) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 42880f8b16..cb19d95418 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1,3 +1,4 @@ +import ast import base64 import logging from datetime import datetime, timedelta @@ -651,6 +652,7 @@ def create_folio(self, pms_folio_info): "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 = [] @@ -704,6 +706,7 @@ def create_folio(self, pms_folio_info): .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) ) @@ -753,7 +756,8 @@ def create_folio(self, pms_folio_info): or reservation_record.board_service_room_id == 0 ): reservation_record.with_context( - skip_compute_service_ids=False + 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 @@ -789,7 +793,9 @@ def create_folio(self, pms_folio_info): date_to=date_to, ) if external_app: - self.env["pms.api.log"].sudo().create( + self.env["pms.api.log"].with_context( + record_ids=folio.ids + ).sudo().create( { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, @@ -799,6 +805,9 @@ def create_folio(self, pms_folio_info): "request_date": fields.Datetime.now(), "method": "POST", "endpoint": "/folios", + "model_id": self.env["ir.model"] + .search([("model", "=", "pms.folio")]) + .id, } ) return folio.id @@ -818,6 +827,9 @@ def create_folio(self, pms_folio_info): "request_date": fields.Datetime.now(), "method": "POST", "endpoint": "/folios", + "model_id": self.env["ir.model"] + .search([("model", "=", "pms.folio")]) + .id, } ) if not external_app: @@ -1590,7 +1602,7 @@ def update_put_external_folio(self, external_reference, pms_folio_info): 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"].create( + self.env["pms.api.log"].with_context(record_ids=folio.ids).sudo().create( { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, @@ -1600,6 +1612,9 @@ def update_put_external_folio(self, external_reference, pms_folio_info): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", + "model_id": self.env["ir.model"] + .search([("model", "=", "pms.folio")]) + .id, } ) return folio.id @@ -1619,6 +1634,9 @@ def update_put_external_folio(self, external_reference, pms_folio_info): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", + "model_id": self.env["ir.model"] + .search([("model", "=", "pms.folio")]) + .id, } ) if not external_app: @@ -1646,7 +1664,7 @@ def update_put_folio(self, folio_id, pms_folio_info): if not folio: raise MissingError(_("Folio not found")) self.update_folio_values(folio, pms_folio_info) - self.env["pms.api.log"].create( + self.env["pms.api.log"].with_context(record_ids=folio.ids).sudo().create( { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, @@ -1656,8 +1674,12 @@ def update_put_folio(self, folio_id, pms_folio_info): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", + "model_id": self.env["ir.model"] + .search([("model", "=", "pms.folio")]) + .id, } ) + return folio.id except Exception as e: _logger.error( @@ -1675,6 +1697,9 @@ def update_put_folio(self, folio_id, pms_folio_info): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", + "model_id": self.env["ir.model"] + .search([("model", "=", "pms.folio")]) + .id, } ) if not external_app: @@ -1684,6 +1709,15 @@ def update_put_folio(self, folio_id, pms_folio_info): def update_folio_values(self, folio, pms_folio_info): external_app = self.env.user.pms_api_client + origin_values_dict = False + if external_app: + origin_values_dict = ast.literal_eval(folio.origin_json) + if origin_values_dict: + # Compare the values of the origin folio with the new values + # and set the new value to None if it is the same as the origin value + for key, value in origin_values_dict: + if value == pms_folio_info[key]: + pms_folio_info[key] = None folio_vals = {} if pms_folio_info.state == "cancel": folio.action_cancel() @@ -1699,9 +1733,15 @@ def update_folio_values(self, folio, pms_folio_info): # reservation.confirm() if ( pms_folio_info.internalComment is not None - and folio.internal_comment != pms_folio_info.internalComment + and pms_folio_info.internalComment not in folio.internal_comment ): - folio_vals.update({"internal_comment": pms_folio_info.internalComment}) + 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: @@ -1735,12 +1775,18 @@ def update_folio_values(self, folio, pms_folio_info): folio_vals.update({"reservation_ids": reservations_vals}) if folio_vals: if reservations_vals: + # Cancel the old reservations that have not been included in the update + update_reservation_ids = [] + for val in reservations_vals: + if val[0] == 1: + update_reservation_ids.append(val[1]) folio.reservation_ids.filtered( - lambda r: r.state != "cancel" + lambda r: r.state != "cancel" and r.id not in update_reservation_ids ).with_context(modified=True, force_write_blocked=True).action_cancel() 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) @@ -1819,34 +1865,83 @@ def wrapper_reservations(self, folio, info_reservations): """ 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 + payload_nights = round( + sum(info_reservation.reservationLines.mapped("price")), 2 + ) + proposed_reservation = saved_reservations.filtered( + lambda r: r.checkin == info_reservation.checkin + and r.checkout == info_reservation.checkout + and r.room_type_id == info_reservation.roomTypeId + ).filtered( + lambda r: round( + sum(r.reservation_line_ids.mapped("price")) + + r.service_ids.filtered(lambda s: s.is_board_service).mapped(""), + 2, + ) + == payload_nights + ) + if proposed_reservation: + saved_reservations -= proposed_reservation vals = {} - vals.update({"folio_id": folio.id}) + new_res = not proposed_reservation + if new_res: + vals.update({"folio_id": folio.id}) if info_reservation.roomTypeId: - vals.update({"room_type_id": 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: - vals.update({"checkin": info_reservation.checkin}) + if new_res or proposed_reservation.checkin != info_reservation.checkin: + vals.update({"checkin": info_reservation.checkin}) if info_reservation.checkout: - vals.update({"checkout": info_reservation.checkout}) + if ( + new_res + or proposed_reservation.checkout != info_reservation.checkout + ): + vals.update({"checkout": info_reservation.checkout}) if info_reservation.pricelistId: - vals.update({"pricelist_id": 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: - vals.update( - { - "board_service_room_id": self.get_board_service_room_type_id( - info_reservation.boardServiceId, - info_reservation.roomTypeId, - folio.pms_property_id.id, - ) - } + 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: - vals.update({"preferred_room_id": info_reservation.preferredRoomId}) + if ( + new_res + or proposed_reservation.preferredRoomId + != info_reservation.preferredRoomId + ): + vals.update({"preferred_room_id": info_reservation.preferredRoomId}) if info_reservation.adults: - vals.update({"adults": info_reservation.adults}) + if new_res or proposed_reservation.adults != info_reservation.adults: + vals.update({"adults": info_reservation.adults}) if info_reservation.children: - vals.update({"children": info_reservation.children}) - if info_reservation.reservationLines: + if ( + new_res + or proposed_reservation.children != info_reservation.children + ): + vals.update({"children": info_reservation.children}) + if info_reservation.reservationLines and new_res: # 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"): @@ -1883,14 +1978,19 @@ def wrapper_reservations(self, folio, info_reservations): vals.update({"reservation_line_ids": reservation_lines_cmds}) if info_reservation.services: reservation_services_cmds = self.wrapper_reservation_services( - info_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 - else: + 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): @@ -1909,17 +2009,27 @@ def wrapper_reservation_lines(self, reservation, board_day_price=0): ) return cmds - def wrapper_reservation_services(self, info_reservations): + def wrapper_reservation_services(self, info_services, services=False): cmds = [] - for service in info_reservations: + 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": service.productId, - "product_qty": service.quantity, - "discount": service.discount or 0, + "product_id": info_service.productId, + "product_qty": info_service.quantity, + "discount": info_service.discount or 0, }, ) ) diff --git a/pms_api_rest/views/pms_api_log_views.xml b/pms_api_rest/views/pms_api_log_views.xml index 75d83467de..ec8b9bcbcf 100644 --- a/pms_api_rest/views/pms_api_log_views.xml +++ b/pms_api_rest/views/pms_api_log_views.xml @@ -21,6 +21,13 @@ pms.api.log
+
+
From 053801378d0b821afa6e741d9b3d4fba0dac44cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 16 Mar 2024 17:46:24 +0100 Subject: [PATCH 507/547] [IMP]pms_api_rest: Imrpovement invoice management, block date and block fields, reverse with origin ref --- pms_api_rest/services/pms_folio_service.py | 1 + pms_api_rest/services/pms_invoice_service.py | 36 ++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index cb19d95418..059066bfca 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1475,6 +1475,7 @@ def get_folio_reservation_messages(self, folio_id): ) def parse_message_body(self, message): + message = message.sudo() message_body = "" if message.body: message_body = message.body diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 0828757617..11319f8204 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -205,6 +205,7 @@ def update_invoice(self, invoice_id, pms_invoice_info): 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 @@ -227,7 +228,7 @@ def update_invoice(self, invoice_id, pms_invoice_info): if cmd_invoice_lines: new_vals["invoice_line_ids"] = cmd_invoice_lines new_invoice = False - if new_vals: + if new_vals and self.check_blocked_fields(invoice, 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 @@ -260,7 +261,13 @@ def update_invoice(self, invoice_id, pms_invoice_info): cmd_new_invoice_lines.append((1, new_id, item[2])) if cmd_new_invoice_lines: new_vals["invoice_line_ids"] = cmd_new_invoice_lines - invoice._reverse_moves(cancel=True) + default_values_list = [ + { + "ref": _(f'Reversal of: {move.name + " - " + move.ref}'), + } + for move in invoice + ] + invoice._reverse_moves(default_values_list, cancel=True) # Update Journal by partner if necessary (simplified invoice -> normal invoice) new_vals["journal_id"] = ( invoice.pms_property_id._get_folio_default_journal( @@ -343,6 +350,26 @@ def _direct_move_update(self, invoice, new_vals): invoice.action_post() return invoice + def check_blocked_fields(self, invoice, new_vals): + # Check partner and amounts + if 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( [ ( @@ -431,6 +458,11 @@ def send_invoice_mail(self, invoice_id, pms_mail_info): "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): From b4118b6479e9893b7ce1dc6a38eb0ed09e1cf9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 17 Mar 2024 19:55:34 +0100 Subject: [PATCH 508/547] [FIX]pms_api_rest: attribute inherit in pms.folio --- pms_api_rest/models/pms_folio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/models/pms_folio.py b/pms_api_rest/models/pms_folio.py index 498b1f3fb6..182f231a94 100644 --- a/pms_api_rest/models/pms_folio.py +++ b/pms_api_rest/models/pms_folio.py @@ -2,7 +2,7 @@ class PmsFolio(models.Model): - _name = "pms.folio" + _inherit = "pms.folio" pms_api_log_id = fields.Many2one( string="PMS API Log", From 109c2b7598ecc05b74bf32582ba7a90e1a2f24ce Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 27 Mar 2024 17:51:09 +0100 Subject: [PATCH 509/547] [IMP]14.0-pms_api_rest: added undoOnboard in reservation datamodel --- pms_api_rest/datamodels/pms_reservation.py | 1 + pms_api_rest/services/pms_reservation_service.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pms_api_rest/datamodels/pms_reservation.py b/pms_api_rest/datamodels/pms_reservation.py index 3e60a8e3e6..035cfd86d8 100644 --- a/pms_api_rest/datamodels/pms_reservation.py +++ b/pms_api_rest/datamodels/pms_reservation.py @@ -74,6 +74,7 @@ class PmsReservationInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 321be80f76..9e9480a9fa 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -287,6 +287,9 @@ def _update_reservation_state(self, reservation, reservation_data): reservation.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, From 1d7260967cafedf30314c25869c2bdaea0a4cab5 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 22 Mar 2024 18:48:33 +0100 Subject: [PATCH 510/547] [IMP]14.0-pms_api_rest: origin input data field in checkin partner modal --- .../datamodels/pms_checkin_partner.py | 1 + pms_api_rest/models/__init__.py | 1 + pms_api_rest/models/pms_checkin_partner.py | 13 +++++ pms_api_rest/services/pms_partner_service.py | 16 +++---- .../services/pms_reservation_service.py | 47 ++++++++++++------- 5 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 pms_api_rest/models/pms_checkin_partner.py diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py index c068422f22..11181da5de 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner.py +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -29,3 +29,4 @@ class PmsCheckinPartnerInfo(Datamodel): 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) diff --git a/pms_api_rest/models/__init__.py b/pms_api_rest/models/__init__.py index 8a708fe07a..1e4c70b33d 100644 --- a/pms_api_rest/models/__init__.py +++ b/pms_api_rest/models/__init__.py @@ -8,3 +8,4 @@ from . import ota_property_settings from . import pms_api_log from . import pms_folio +from . import pms_checkin_partner 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..3305d479f7 --- /dev/null +++ b/pms_api_rest/models/pms_checkin_partner.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class PmsCheckinPartner(models.Model): + _inherit = 'pms.checkin.partner' + + origin_input_data = fields.Selection([ + ('wizard', 'Wizard'), + ('form', 'Form'), + ('regular_customer', 'Regular Customer'), + ('ocr', 'OCR'), + ('precheckin', 'Precheckin'), + ], string='Origin Input Data') diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 8e10f4034f..87c9d7f109 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -507,15 +507,7 @@ def get_partner_by_doc_number(self, document_type, document_number): doc_number = partner.id_numbers.filtered( lambda doc: doc.category_id.id == doc_type.id ) - PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] - - document_expedition_date = False - if doc_number.valid_from: - document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") - birthdate_date = False - if partner.birthdate_date: - birthdate_date = partner.birthdate_date.strftime("%d/%m/%Y") partners.append( PmsCheckinPartnerInfo( partnerId=partner.id or None, @@ -527,10 +519,14 @@ def get_partner_by_doc_number(self, document_type, document_number): mobile=partner.mobile or None, documentType=doc_type.id or None, documentNumber=doc_number.name or None, - documentExpeditionDate=document_expedition_date or None, + documentExpeditionDate=datetime.combine( + doc_number.valid_from, datetime.min.time() + ).isoformat() if doc_number.valid_from else None, documentSupportNumber=doc_number.support_number or None, gender=partner.gender or None, - birthdate=birthdate_date 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, diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 9e9480a9fa..26d3f1d5ef 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -564,12 +564,6 @@ def get_checkin_partners(self, reservation_id): # lambda ch: ch.state != "dummy" # ) for checkin_partner in reservation.checkin_partner_ids: - if checkin_partner.document_expedition_date: - document_expedition_date = ( - checkin_partner.document_expedition_date.strftime("%d/%m/%Y") - ) - if checkin_partner.birthdate_date: - birthdate_date = checkin_partner.birthdate_date.strftime("%d/%m/%Y") checkin_partners.append( PmsCheckinPartnerInfo( id=checkin_partner.id, @@ -592,14 +586,18 @@ def get_checkin_partners(self, reservation_id): documentNumber=checkin_partner.document_number if checkin_partner.document_number else None, - documentExpeditionDate=document_expedition_date + 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, gender=checkin_partner.gender if checkin_partner.gender else "", - birthdate=birthdate_date + birthdate=datetime.combine( + checkin_partner.birthdate_date, datetime.min.time() + ).isoformat() if checkin_partner.birthdate_date else None, residenceStreet=checkin_partner.residence_street @@ -653,9 +651,16 @@ def write_reservation_checkin_partner( and pms_checkin_partner_info.actionOnBoard is not None ): checkin_partner.action_on_board() + return checkin_partner.id checkin_partner.write( - self.mapping_checkin_partner_values(pms_checkin_partner_info) + 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( @@ -850,8 +855,14 @@ def create_reservation_checkin_partner( checkin_partner_last_id ) checkin_partner.write( - self.mapping_checkin_partner_values(pms_checkin_partner_info) + 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( @@ -870,9 +881,8 @@ def delete_reservation_checkin_partner(self, reservation_id, checkin_partner_id) if checkin_partner: checkin_partner.unlink() - def mapping_checkin_partner_values(self, pms_checkin_partner_info): - vals = dict() - checkin_partner_fields = { + 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, @@ -888,22 +898,26 @@ def mapping_checkin_partner_values(self, pms_checkin_partner_info): "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}) - for k, v in checkin_partner_fields.items(): - if v: - vals.update({k: v}) + else: + vals.update({"birthdate_date": False}) return vals @restapi.method( @@ -1306,4 +1320,3 @@ def wizard_states(self, reservation_id): text='', ) - From d7dbf6283623df65b39ab087b649cfd0e7496932 Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 27 Mar 2024 12:39:06 +0100 Subject: [PATCH 511/547] [IMP]14.0-pms_api_rest: document country field added to checkin partner datamodel & priority field in document type datamodel --- pms_api_rest/datamodels/pms_checkin_partner.py | 1 + pms_api_rest/datamodels/pms_id_category.py | 1 + pms_api_rest/services/pms_id_category_service.py | 3 ++- pms_api_rest/services/pms_partner_service.py | 1 + pms_api_rest/services/pms_reservation_service.py | 4 ++++ 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py index 11181da5de..2b5a558bb0 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner.py +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -18,6 +18,7 @@ class PmsCheckinPartnerInfo(Datamodel): 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) diff --git a/pms_api_rest/datamodels/pms_id_category.py b/pms_api_rest/datamodels/pms_id_category.py index 749fb38721..eeb1ab43b3 100644 --- a/pms_api_rest/datamodels/pms_id_category.py +++ b/pms_api_rest/datamodels/pms_id_category.py @@ -8,3 +8,4 @@ class PmsIdCategoryInfo(Datamodel): 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/services/pms_id_category_service.py b/pms_api_rest/services/pms_id_category_service.py index bcfe7f4a8d..8568c0f36b 100644 --- a/pms_api_rest/services/pms_id_category_service.py +++ b/pms_api_rest/services/pms_id_category_service.py @@ -27,13 +27,14 @@ def get_id_categories(self): for id_category in ( self.env["res.partner.id_category"] .with_context(lang=self.env.user.lang) - .search([]) + .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_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 87c9d7f109..488fc4f506 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -523,6 +523,7 @@ def get_partner_by_doc_number(self, document_type, document_number): doc_number.valid_from, datetime.min.time() ).isoformat() if doc_number.valid_from else None, documentSupportNumber=doc_number.support_number or None, + documentCountryId=doc_number.country_id.id or None, gender=partner.gender or None, birthdate=datetime.combine( partner.birthdate_date, datetime.min.time() diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 26d3f1d5ef..1ad3a0cd17 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -594,6 +594,9 @@ def get_checkin_partners(self, reservation_id): 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() @@ -890,6 +893,7 @@ def mapping_checkin_partner_values(self, pms_checkin_partner_info, partner_id=Fa "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, From 478dec43fb19b080082aabe543cc001e63a59af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 13 Apr 2024 12:41:19 +0200 Subject: [PATCH 512/547] [FIX]pms_api_rest: partner service date format parameters --- pms_api_rest/services/pms_partner_service.py | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 488fc4f506..b8760b0d6e 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -198,7 +198,6 @@ def get_partners(self, pms_partner_search_params): ) doc_number = False document_number = False - document_expedition_date = False document_type = False document_support_number = False if partner.id_numbers: @@ -208,8 +207,6 @@ def get_partners(self, pms_partner_search_params): document_number = doc_number.name if doc_number.category_id: document_type = doc_number.category_id.id - if doc_number.valid_from: - document_expedition_date = doc_number.valid_from.strftime("%d/%m/%Y") if doc_number.support_number: document_support_number = doc_number.support_number result_partners.append( @@ -260,7 +257,9 @@ def get_partners(self, pms_partner_search_params): 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, + documentSupportNumber=document_support_number + if document_support_number + else None, vatNumber=partner.vat if partner.vat else partner.aeat_identification @@ -271,8 +270,10 @@ def get_partners(self, pms_partner_search_params): else partner.aeat_identification_type if partner.aeat_identification_type else None, - documentExpeditionDate=document_expedition_date - if document_expedition_date + documentExpeditionDate=datetime.combine( + doc_number.valid_from, datetime.min.time() + ).isoformat() + if doc_number and doc_number.valid_from else None, comment=partner.comment if partner.comment else None, language=partner.lang if partner.lang else None, @@ -521,13 +522,17 @@ def get_partner_by_doc_number(self, document_type, document_number): documentNumber=doc_number.name or None, documentExpeditionDate=datetime.combine( doc_number.valid_from, datetime.min.time() - ).isoformat() if doc_number.valid_from else None, + ).isoformat() + if doc_number.valid_from + else None, documentSupportNumber=doc_number.support_number or None, documentCountryId=doc_number.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, + ).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, From 3ee5e62b521fabc76b1c98cf5103b544fc15716d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 13 Apr 2024 12:42:13 +0200 Subject: [PATCH 513/547] [ADD]pms_api_rest: service traveller report --- pms_api_rest/services/pms_property_service.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index daea48b1a2..fa0e770f85 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -1,3 +1,7 @@ +import base64 + +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 @@ -133,3 +137,35 @@ def get_users(self, pms_property_id): ) ) return result_users + + @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) From b9d07b538125aaf444899ed510ed4de01998add1 Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 15 Apr 2024 14:52:54 +0200 Subject: [PATCH 514/547] [IMP]14.0-pms_api_rest: documentCountryId field in partner datamodel --- pms_api_rest/datamodels/pms_partner.py | 1 + pms_api_rest/services/pms_partner_service.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pms_api_rest/datamodels/pms_partner.py b/pms_api_rest/datamodels/pms_partner.py index 7c6f33a414..ac38153732 100644 --- a/pms_api_rest/datamodels/pms_partner.py +++ b/pms_api_rest/datamodels/pms_partner.py @@ -32,6 +32,7 @@ class PmsPartnerInfo(Datamodel): 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) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index b8760b0d6e..a43197d0b5 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -260,6 +260,7 @@ def get_partners(self, pms_partner_search_params): documentSupportNumber=document_support_number if document_support_number else None, + documentCountryId=doc_number.country_id.id if doc_number.country_id else None, vatNumber=partner.vat if partner.vat else partner.aeat_identification From a89d4d6264bc398344a42b85e7c033a288c045ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 17 Apr 2024 11:27:24 +0200 Subject: [PATCH 515/547] [IMP]pms_api_rest: Imrpovement API logs, PUT folio service and action_confirm reservation --- pms_api_rest/__manifest__.py | 1 + pms_api_rest/data/cron_jobs.xml | 20 ++ pms_api_rest/models/pms_api_log.py | 74 ++-- pms_api_rest/models/pms_folio.py | 13 +- pms_api_rest/models/pms_property.py | 170 ++++++---- pms_api_rest/services/pms_folio_service.py | 318 +++++++++++------- .../services/pms_reservation_service.py | 205 +++++------ 7 files changed, 483 insertions(+), 318 deletions(-) create mode 100644 pms_api_rest/data/cron_jobs.xml diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 80661e978d..73f298c565 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -25,6 +25,7 @@ "data/sql_reports.xml", "data/auth_jwt_validator.xml", "data/pms_app_reset_password_template.xml", + "data/cron_jobs.xml", "views/pms_property_views.xml", "views/res_users_views.xml", "views/pms_room_type_class_views.xml", diff --git a/pms_api_rest/data/cron_jobs.xml b/pms_api_rest/data/cron_jobs.xml new file mode 100644 index 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/models/pms_api_log.py b/pms_api_rest/models/pms_api_log.py index 9e3dcb2bff..0b0a9f7ae8 100644 --- a/pms_api_rest/models/pms_api_log.py +++ b/pms_api_rest/models/pms_api_log.py @@ -1,4 +1,6 @@ -from odoo import _, api, fields, models +from datetime import timedelta + +from odoo import _, fields, models class PmsApiLog(models.Model): @@ -72,10 +74,39 @@ class PmsApiLog(models.Model): string="Response URL", help="Response URL", ) - model_id = fields.Many2one( - string="Model", - help="Model", - comodel_name="ir.model", + request_type = fields.Selection( + string="Request Type", + help="Request Type", + selection=[ + ("folios", "Folios"), + ("availability", "Availability"), + ("restrictions", "Restrictions rules"), + ("prices", "Prices"), + ], + ) + target_date_from = fields.Date( + string="Target Date From", + help="Target Date From", + ) + target_date_to = fields.Date( + string="Target Date To", + help="Target Date To", + ) + folio_ids = fields.Many2many( + string="Folios", + help="Folios", + comodel_name="pms.folio", + relation="pms_folio_pms_api_log_rel", + column1="pms_api_log_ids", + column2="folio_ids", + ) + room_type_ids = fields.Many2many( + string="Room Types", + help="Room Types", + comodel_name="pms.room.type", + relation="pms_room_type_pms_api_log_rel", + column1="pms_api_log_ids", + column2="room_type_ids", ) def related_action_open_record(self): @@ -90,10 +121,7 @@ def related_action_open_record(self): """ self.ensure_one() - if "pms_api_log_id" in self.env[self.model_id.model]._fields: - records = self.env[self.model_id.model].search( - [("pms_api_log_id", "=", self.id)] - ) + records = self.folio_ids if not records: return None action = { @@ -114,21 +142,15 @@ def related_action_open_record(self): ) return action - @api.model - def create(self, vals): - """ - set pms_api_log_id and origin_json in related records - if record_ids id present in context + 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. + """ - log_record = super().create(vals) - if self.env.context.get("record_ids"): - records = self.env[self.env.context.get("model")].browse( - self.env.context.get("record_ids") - ) - records.write( - { - "pms_api_log_id": log_record.id, - "origin_json": log_record.request, - } - ) - return log_record + self.sudo().search( + [ + ("status", "=", "success"), + ("create_date", "<", fields.Datetime.now() - timedelta(days=offset)), + ] + ).unlink() diff --git a/pms_api_rest/models/pms_folio.py b/pms_api_rest/models/pms_folio.py index 182f231a94..b0ff25c590 100644 --- a/pms_api_rest/models/pms_folio.py +++ b/pms_api_rest/models/pms_folio.py @@ -4,12 +4,11 @@ class PmsFolio(models.Model): _inherit = "pms.folio" - pms_api_log_id = fields.Many2one( - string="PMS API Log", - help="PMS API Log", + pms_api_log_ids = fields.Many2many( + string="API Logs", + help="API Logs", comodel_name="pms.api.log", - ) - origin_json = fields.Text( - string="Origin JSON", - help="Origin JSON", + relation="pms_folio_pms_api_log_rel", + column1="folio_ids", + column2="pms_api_log_ids", ) diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index 45d558330e..384249d113 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -573,6 +573,9 @@ def pms_api_push_batch( clients = client else: clients = self.env["res.users"].search([("pms_api_client", "=", True)]) + room_type_ids = [] + endpoint = "" + response = None _logger.info("PMS API push batch") if isinstance(date_from, str): date_from = datetime.datetime.strptime(date_from, "%Y-%m-%d").date() @@ -593,73 +596,112 @@ def pms_api_push_batch( ] ) for pms_property in pms_properties: - property_client_conf = self.env["ota.property.settings"].search( - [ - ("pms_property_id", "=", pms_property.id), - ("agency_id", "=", client.partner_id.id), - ] - ) - pms_property_id = pms_property.id - room_type_ids = ( - [filter_room_type_id] - if filter_room_type_id - else self.env["pms.room"] - .search([("pms_property_id", "=", pms_property_id)]) - .mapped("room_type_id") - .filtered( - lambda r: r.id - not in property_client_conf.excluded_room_type_ids.ids + try: + property_client_conf = ( + self.env["ota.property.settings"] + .sudo() + .search( + [ + ("pms_property_id", "=", pms_property.id), + ("agency_id", "=", client.partner_id.id), + ] + ) ) - .ids - ) - payload = { - "pmsPropertyId": pms_property_id, - } - data = [] - for room_type_id in room_type_ids: - if call_type == "availability": - endpoint = client.url_endpoint_availability - data.extend( - pms_property.generate_availability_json( - date_from=date_from, - date_to=date_to, - pms_property_id=pms_property_id, - room_type_id=room_type_id, - client=client, - ) + pms_property_id = pms_property.id + room_type_ids = ( + [filter_room_type_id] + if filter_room_type_id + else self.env["pms.room"] + .search([("pms_property_id", "=", pms_property_id)]) + .mapped("room_type_id") + .filtered( + lambda r: r.id + not in property_client_conf.excluded_room_type_ids.ids ) - key_data = "avails" - elif call_type == "restrictions": - endpoint = client.url_endpoint_rules - data.extend( - pms_property.generate_restrictions_json( - date_from=date_from, - date_to=date_to, - pms_property_id=pms_property_id, - room_type_id=room_type_id, - client=client, + .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 = "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 = "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}""" ) - key_data = "prices" - else: - raise ValidationError(_("Invalid call type")) - if data: - payload[key_data] = data - response = self.pms_api_push_payload(payload, endpoint, client) - _logger.info( - f"""PMS API push batch response to - {endpoint}: {response.status_code} - {response.text}""" + self.invalidate_cache() + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": pms_property_id, + "client_id": client.id, + "request": payload, + "response": str(response), + "status": "success" if response.ok else "error", + "request_date": fields.Datetime.now(), + "method": "PUSH", + "endpoint": endpoint, + "target_date_from": date_from, + "target_date_to": date_to, + "request_type": call_type, + "room_type_ids": room_type_ids, + } + ) + except Exception as e: + _logger.error(f"""PMS API push batch error: {e}""") + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": pms_property_id, + "client_id": client.id, + "request": payload, + "response": str(e), + "status": "error", + "request_date": fields.Datetime.now(), + "method": "PUSH", + "endpoint": endpoint, + "target_date_from": date_from, + "target_date_to": date_to, + "request_type": call_type, + "room_type_ids": room_type_ids, + } ) - self.invalidate_cache() diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 059066bfca..d0269de9ee 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1,4 +1,3 @@ -import ast import base64 import logging from datetime import datetime, timedelta @@ -584,6 +583,12 @@ def get_folio_reservations(self, folio_id): def create_folio(self, pms_folio_info): external_app = self.env.user.pms_api_client log_payload = pms_folio_info + min_checkin_payload = min( + pms_folio_info.reservations, key=lambda x: x.checkin + ).checkin + max_checkout_payload = max( + pms_folio_info.reservations, key=lambda x: x.checkout + ).checkout try: if pms_folio_info.reservationType == "out": vals = { @@ -760,7 +765,7 @@ def create_folio(self, pms_folio_info): force_write_blocked=True if external_app else False, )._compute_board_service_room_id() pms_folio_info.transactions = self.normalize_payments_structure( - pms_folio_info + pms_folio_info, folio ) if pms_folio_info.transactions: self.compute_transactions(folio, pms_folio_info.transactions) @@ -793,9 +798,7 @@ def create_folio(self, pms_folio_info): date_to=date_to, ) if external_app: - self.env["pms.api.log"].with_context( - record_ids=folio.ids - ).sudo().create( + self.env["pms.api.log"].sudo().create( { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, @@ -805,9 +808,10 @@ def create_folio(self, pms_folio_info): "request_date": fields.Datetime.now(), "method": "POST", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": folio.ids, + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) return folio.id @@ -827,9 +831,10 @@ def create_folio(self, pms_folio_info): "request_date": fields.Datetime.now(), "method": "POST", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": [], + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) if not external_app: @@ -844,52 +849,62 @@ def compute_transactions(self, folio, transactions): reference += transaction.reference else: raise ValidationError(_("The transaction reference is required")) - if not self.env["account.payment"].search( + proposed_transaction = self.env["account.payment"].search( [ ("pms_property_id", "=", folio.pms_property_id.id), ("payment_type", "=", transaction.transactionType), ("folio_ids", "in", folio.id), - ("ref", "ilike", transaction.reference), + ("ref", "ilike", reference), + ("state", "=", "posted"), ] + ) + if ( + not proposed_transaction + or proposed_transaction.amount != transaction.amount ): - journal = self.env["account.journal"].search( - [("id", "=", transaction.journalId)] - ) - if not journal: - ota_conf = self.env["ota.property.settings"].search( - [ - ("pms_property_id", "=", folio.pms_property_id.id), - ("agency_id", "=", self.env.user.partner_id.id), - ] - ) - if ota_conf: - journal = ota_conf.pms_api_payment_journal_id - if transaction.transactionType == "inbound": - folio.do_payment( - journal, - journal.suspense_account_id, - self.env.user, - transaction.amount, - folio, - reservations=False, - services=False, - partner=False, - date=datetime.strptime(transaction.date, "%Y-%m-%d"), - ref=reference, - ) - elif transaction.transactionType == "outbound": - folio.do_refund( - journal, - journal.suspense_account_id, - self.env.user, - transaction.amount, - folio, - reservations=False, - services=False, - partner=False, - date=datetime.strptime(transaction.date, "%Y-%m-%d"), - ref=reference, + if proposed_transaction: + proposed_transaction.action_draft() + proposed_transaction.amount = transaction.amount + proposed_transaction.action_post() + else: + journal = self.env["account.journal"].search( + [("id", "=", transaction.journalId)] ) + if not journal: + ota_conf = self.env["ota.property.settings"].search( + [ + ("pms_property_id", "=", folio.pms_property_id.id), + ("agency_id", "=", self.env.user.partner_id.id), + ] + ) + if ota_conf: + journal = ota_conf.pms_api_payment_journal_id + if transaction.transactionType == "inbound": + folio.do_payment( + journal, + journal.suspense_account_id, + self.env.user, + transaction.amount, + folio, + reservations=False, + services=False, + partner=False, + date=datetime.strptime(transaction.date, "%Y-%m-%d"), + ref=reference, + ) + elif transaction.transactionType == "outbound": + folio.do_refund( + journal, + journal.suspense_account_id, + self.env.user, + transaction.amount, + folio, + reservations=False, + services=False, + partner=False, + date=datetime.strptime(transaction.date, "%Y-%m-%d"), + ref=reference, + ) @restapi.method( [ @@ -913,7 +928,7 @@ def update_folio(self, folio_id, pms_folio_info): folio.action_cancel() if pms_folio_info.confirmReservations: for reservation in folio.reservation_ids: - reservation.confirm() + reservation.action_confirm() if pms_folio_info.internalComment is not None: folio_vals.update({"internal_comment": pms_folio_info.internalComment}) if pms_folio_info.partnerId: @@ -1593,6 +1608,12 @@ def get_board_service_room_type_id( def update_put_external_folio(self, external_reference, pms_folio_info): external_app = self.env.user.pms_api_client log_payload = pms_folio_info + min_checkin_payload = min( + pms_folio_info.reservations, key=lambda x: x.checkin + ).checkin + max_checkout_payload = max( + pms_folio_info.reservations, key=lambda x: x.checkout + ).checkout try: folio = self.env["pms.folio"].search( [ @@ -1603,7 +1624,7 @@ def update_put_external_folio(self, external_reference, pms_folio_info): if not folio or len(folio) > 1: raise MissingError(_("Folio not found")) self.update_folio_values(folio, pms_folio_info) - self.env["pms.api.log"].with_context(record_ids=folio.ids).sudo().create( + self.env["pms.api.log"].sudo().create( { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, @@ -1613,9 +1634,10 @@ def update_put_external_folio(self, external_reference, pms_folio_info): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": folio.ids, + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) return folio.id @@ -1635,9 +1657,10 @@ def update_put_external_folio(self, external_reference, pms_folio_info): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": [], + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) if not external_app: @@ -1660,12 +1683,18 @@ def update_put_external_folio(self, external_reference, pms_folio_info): def update_put_folio(self, folio_id, pms_folio_info): external_app = self.env.user.pms_api_client log_payload = pms_folio_info + min_checkin_payload = min( + pms_folio_info.reservations, key=lambda x: x.checkin + ).checkin + max_checkout_payload = max( + pms_folio_info.reservations, key=lambda x: x.checkout + ).checkout try: folio = self.env["pms.folio"].browse(folio_id) if not folio: raise MissingError(_("Folio not found")) self.update_folio_values(folio, pms_folio_info) - self.env["pms.api.log"].with_context(record_ids=folio.ids).sudo().create( + self.env["pms.api.log"].sudo().create( { "pms_property_id": pms_folio_info.pmsPropertyId, "client_id": self.env.user.id, @@ -1675,9 +1704,10 @@ def update_put_folio(self, folio_id, pms_folio_info): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": folio.ids, + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) @@ -1698,9 +1728,10 @@ def update_put_folio(self, folio_id, pms_folio_info): "request_date": fields.Datetime.now(), "method": "PUT", "endpoint": "/folios", - "model_id": self.env["ir.model"] - .search([("model", "=", "pms.folio")]) - .id, + "folio_ids": [], + "target_date_from": min_checkin_payload, + "target_date_to": max_checkout_payload, + "request_type": "folios", } ) if not external_app: @@ -1710,17 +1741,11 @@ def update_put_folio(self, folio_id, pms_folio_info): def update_folio_values(self, folio, pms_folio_info): external_app = self.env.user.pms_api_client - origin_values_dict = False - if external_app: - origin_values_dict = ast.literal_eval(folio.origin_json) - if origin_values_dict: - # Compare the values of the origin folio with the new values - # and set the new value to None if it is the same as the origin value - for key, value in origin_values_dict: - if value == pms_folio_info[key]: - pms_folio_info[key] = None folio_vals = {} - if pms_folio_info.state == "cancel": + if pms_folio_info.state == "cancel" and folio.state != "cancel": + draft_invoices = folio.invoice_ids.filtered(lambda i: i.state == "draft") + if draft_invoices: + draft_invoices.action_cancel() folio.action_cancel() return folio.id # if ( @@ -1731,7 +1756,7 @@ def update_folio_values(self, folio, pms_folio_info): # ) # ): # for reservation in folio.reservation_ids: - # reservation.confirm() + # reservation.action_confirm() if ( pms_folio_info.internalComment is not None and pms_folio_info.internalComment not in folio.internal_comment @@ -1765,32 +1790,49 @@ def update_folio_values(self, folio, pms_folio_info): folio_vals.update({"mobile": pms_folio_info.partnerPhone}) if ( self.get_language(pms_folio_info.language) - and self.get_language(pms_folio_info.language) != pms_folio_info.language + and self.get_language(pms_folio_info.language) != folio.lang ): folio_vals.update({"lang": self.get_language(pms_folio_info.language)}) + reservations_vals = [] if pms_folio_info.reservations: reservations_vals = self.wrapper_reservations( folio, pms_folio_info.reservations ) if reservations_vals: - folio_vals.update({"reservation_ids": reservations_vals}) - if folio_vals: - if reservations_vals: - # Cancel the old reservations that have not been included in the update update_reservation_ids = [] for val in reservations_vals: + # Cancel the old reservations that have not been included in the update if val[0] == 1: + if val[2].get("state") == "cancel": + self.env["pms.reservation"].with_context( + force_write_blocked=True + ).browse(val[1]).action_cancel() + # delete from reservations_vals the reservation that has been canceled + reservations_vals.pop(reservations_vals.index(val)) + if val[2].get("state") == "confirm": + self.env["pms.reservation"].with_context( + force_write_blocked=True + ).browse(val[1]).action_confirm() + # delete from reservations_vals the field state + val[2].pop("state") update_reservation_ids.append(val[1]) - folio.reservation_ids.filtered( + old_reservations_to_cancel = folio.reservation_ids.filtered( lambda r: r.state != "cancel" and r.id not in update_reservation_ids - ).with_context(modified=True, force_write_blocked=True).action_cancel() + ) + old_reservations_to_cancel.with_context( + modified=True, force_write_blocked=True + ).action_cancel() + folio_vals.update({"reservation_ids": reservations_vals}) + if folio_vals: folio.with_context( skip_compute_service_ids=False if external_app else True, force_overbooking=True if external_app else False, force_write_blocked=True if external_app else False, ).write(folio_vals) # Compute OTA transactions - pms_folio_info.transactions = self.normalize_payments_structure(pms_folio_info) + pms_folio_info.transactions = self.normalize_payments_structure( + pms_folio_info, folio + ) if pms_folio_info.transactions: self.compute_transactions(folio, pms_folio_info.transactions) # Force update availability @@ -1804,7 +1846,7 @@ def update_folio_values(self, folio, pms_folio_info): date_to=date_to, ) - def normalize_payments_structure(self, pms_folio_info): + def normalize_payments_structure(self, pms_folio_info, folio): """ This method use the OTA payment structure to normalize the structure and incorporate them in the transactions datamodel param @@ -1821,6 +1863,17 @@ def normalize_payments_structure(self, pms_folio_info): ("agency_id", "=", self.env.user.partner_id.id), ] ) + if not ota_conf: + raise ValidationError( + _("No OTA configuration found for this property") + ) + if not ota_conf.pms_api_payment_journal_id: + raise ValidationError( + _( + "No payment journal configured for this property for %s" + % ota_conf.name + ) + ) transaction.journalId = ota_conf.pms_api_payment_journal_id.id elif pms_folio_info.agencyId: ota_conf = self.env["ota.property.settings"].search( @@ -1849,7 +1902,7 @@ def normalize_payments_structure(self, pms_folio_info): pmsTransactionInfo( journalId=journal.id, transactionType="inbound", - amount=pms_folio_info.totalPrice, + amount=round(folio.amount_total, 2), date=fields.Date.today().strftime("%Y-%m-%d"), reference=pms_folio_info.externalReference, ) @@ -1871,22 +1924,17 @@ def wrapper_reservations(self, folio, 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 - payload_nights = round( - sum(info_reservation.reservationLines.mapped("price")), 2 - ) proposed_reservation = saved_reservations.filtered( - lambda r: r.checkin == info_reservation.checkin - and r.checkout == info_reservation.checkout - and r.room_type_id == info_reservation.roomTypeId - ).filtered( - lambda r: round( - sum(r.reservation_line_ids.mapped("price")) - + r.service_ids.filtered(lambda s: s.is_board_service).mapped(""), - 2, - ) - == payload_nights + lambda r: r.checkin + == datetime.strptime(info_reservation.checkin, "%Y-%m-%d").date() + and r.checkout + == datetime.strptime(info_reservation.checkout, "%Y-%m-%d").date() + and r.room_type_id.id == info_reservation.roomTypeId + and r.adults == info_reservation.adults + and r.children == info_reservation.children ) if proposed_reservation: + proposed_reservation = proposed_reservation[0] saved_reservations -= proposed_reservation vals = {} new_res = not proposed_reservation @@ -1900,12 +1948,17 @@ def wrapper_reservations(self, folio, info_reservations): ): vals.update({"room_type_id": info_reservation.roomTypeId}) if info_reservation.checkin: - if new_res or proposed_reservation.checkin != info_reservation.checkin: + if ( + new_res + or proposed_reservation.checkin + != datetime.strptime(info_reservation.checkin, "%Y-%m-%d").date() + ): vals.update({"checkin": info_reservation.checkin}) if info_reservation.checkout: if ( new_res - or proposed_reservation.checkout != info_reservation.checkout + or proposed_reservation.checkout + != datetime.strptime(info_reservation.checkout, "%Y-%m-%d").date() ): vals.update({"checkout": info_reservation.checkout}) if info_reservation.pricelistId: @@ -1929,7 +1982,7 @@ def wrapper_reservations(self, folio, info_reservations): if info_reservation.preferredRoomId: if ( new_res - or proposed_reservation.preferredRoomId + or proposed_reservation.preferred_room_id.id != info_reservation.preferredRoomId ): vals.update({"preferred_room_id": info_reservation.preferredRoomId}) @@ -1942,7 +1995,9 @@ def wrapper_reservations(self, folio, info_reservations): or proposed_reservation.children != info_reservation.children ): vals.update({"children": info_reservation.children}) - if info_reservation.reservationLines and new_res: + if new_res or info_reservation.stateCode != proposed_reservation.state: + vals.update({"state": info_reservation.stateCode}) + if info_reservation.reservationLines: # The service price is included in day price when it is a board service (external api) board_day_price = 0 if external_app and vals.get("board_service_room_id"): @@ -1974,6 +2029,7 @@ def wrapper_reservations(self, folio, info_reservations): reservation_lines_cmds = self.wrapper_reservation_lines( reservation=info_reservation, board_day_price=board_day_price, + proposed_reservation=proposed_reservation, ) if reservation_lines_cmds: vals.update({"reservation_line_ids": reservation_lines_cmds}) @@ -1994,20 +2050,38 @@ def wrapper_reservations(self, folio, info_reservations): cmds.append((1, proposed_reservation.id, vals)) return cmds - def wrapper_reservation_lines(self, reservation, board_day_price=0): + def wrapper_reservation_lines( + self, reservation, board_day_price=0, proposed_reservation=False + ): cmds = [] for line in reservation.reservationLines: - cmds.append( - ( - 0, - False, - { - "date": line.date, - "price": line.price - board_day_price, - "discount": line.discount or 0, - }, + if proposed_reservation: + # Not is necesay check new dates, becouse a if the dates change, the reservation is new + proposed_line = proposed_reservation.reservation_line_ids.filtered( + lambda l: l.date == datetime.strptime(line.date, "%Y-%m-%d").date() ) - ) + if proposed_line: + vals = {} + if round(proposed_line.price, 2) != round( + line.price - board_day_price, 2 + ): + vals.update({"price": line.price - board_day_price}) + if round(proposed_line.discount, 2) != round(line.discount, 2): + vals.update({"discount": line.discount}) + if vals: + cmds.append((1, proposed_line.id, vals)) + else: + cmds.append( + ( + 0, + False, + { + "date": line.date, + "price": line.price - board_day_price, + "discount": line.discount or 0, + }, + ) + ) return cmds def wrapper_reservation_services(self, info_services, services=False): @@ -2045,17 +2119,21 @@ def force_api_update_avail( It is used to override potential availability changes on the channel made unilaterally, for example, upon entering or canceling a reservation. """ - api_clients = self.env["res.users"].search( - [ - ("pms_api_client", "=", True), - ("pms_property_ids", "in", pms_property_id), - ] + api_clients = ( + self.env["res.users"] + .sudo() + .search( + [ + ("pms_api_client", "=", True), + ("pms_property_ids", "in", pms_property_id), + ] + ) ) if not room_type_ids or not api_clients: return False for room_type_id in room_type_ids: pms_property = self.env["pms.property"].browse(pms_property_id) - self.env["pms.property"].pms_api_push_batch( + self.env["pms.property"].sudo().pms_api_push_batch( call_type="availability", # 'availability', 'prices', 'restrictions' date_from=date_from.strftime("%Y-%m-%d"), # 'YYYY-MM-DD' date_to=date_to.strftime("%Y-%m-%d"), # 'YYYY-MM-DD' diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 1ad3a0cd17..f6c771270a 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -3,12 +3,12 @@ from odoo import _, fields from odoo.exceptions import MissingError +from odoo.osv import expression +from odoo.tools.safe_eval import safe_eval from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel from odoo.addons.component.core import Component -from odoo.osv import expression -from odoo.tools.safe_eval import safe_eval class PmsReservationService(Component): @@ -284,13 +284,12 @@ def _update_reservation_state(self, reservation, reservation_data): if reservation_data.stateCode == "cancel": reservation.action_cancel() if reservation_data.stateCode == "confirm": - reservation.confirm() + reservation.action_confirm() if reservation_data.toCheckout is not None and reservation_data.toCheckout: reservation.action_reservation_checkout() if reservation_data.undoOnboard: reservation.action_undo_onboard() - def _get_reservation_lines_mapped(self, origin_data, reservation_line=False): # Return dict witch reservation.lines values (only modified if line exist, # or all pass values if line not exist) @@ -587,7 +586,8 @@ def get_checkin_partners(self, reservation_id): if checkin_partner.document_number else None, documentExpeditionDate=datetime.combine( - checkin_partner.document_expedition_date, datetime.min.time() + checkin_partner.document_expedition_date, + datetime.min.time(), ).isoformat() if checkin_partner.document_expedition_date else None, @@ -658,7 +658,7 @@ def write_reservation_checkin_partner( checkin_partner.write( self.mapping_checkin_partner_values( pms_checkin_partner_info, - checkin_partner.partner_id.id if checkin_partner.partner_id else False + checkin_partner.partner_id.id if checkin_partner.partner_id else False, ) ) # if not partner_id we need to force compute to create partner @@ -860,7 +860,9 @@ def create_reservation_checkin_partner( checkin_partner.write( self.mapping_checkin_partner_values( pms_checkin_partner_info, - checkin_partner.partner_id.id if checkin_partner.partner_id else False + checkin_partner.partner_id.id + if checkin_partner.partner_id + else False, ) ) # if not partner_id we need to force compute to create partner @@ -884,7 +886,9 @@ def delete_reservation_checkin_partner(self, reservation_id, checkin_partner_id) if checkin_partner: checkin_partner.unlink() - def mapping_checkin_partner_values(self, pms_checkin_partner_info, partner_id=False): + def mapping_checkin_partner_values( + self, pms_checkin_partner_info, partner_id=False + ): vals = { "firstname": pms_checkin_partner_info.firstname, "lastname": pms_checkin_partner_info.lastname, @@ -1099,17 +1103,17 @@ def departures_report(self, pms_report_search_param): ) def wizard_states(self, reservation_id): reservation = self.env["pms.reservation"].search([("id", "=", reservation_id)]) - today = datetime.now().strftime('%Y-%m-%d') + today = datetime.now().strftime("%Y-%m-%d") wizard_states = [ { "code": "overbooking_with_availability", "title": "Overbooking", "domain": "[" - "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " - "('overbooking', '=', True), " - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " + "('overbooking', '=', True), " + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: r.count_alternative_free_rooms", "text": f"Parece que ha entrado una reserva sin haber disponibilidad para {reservation.room_type_id.name}.", "priority": 100, @@ -1118,64 +1122,64 @@ def wizard_states(self, reservation_id): "code": "overbooking_without_availability", "title": "Overbooking", "domain": "[" - "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " - "('overbooking', '=', True), " - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + "('state', 'in', ['draft', 'confirm', 'arrival_delayed']), " + "('overbooking', '=', True), " + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: r.count_alternative_free_rooms <= 0", "text": f"Parece que ha entrado una reserva sin haber disponibilidad para {reservation.room_type_id.name}." - f"Por desgracia no parece que hay ninguna " - f"habitación disponible con la capacidad suficiente para esta reserva", + f"Por desgracia no parece que hay ninguna " + f"habitación disponible con la capacidad suficiente para esta reserva", "priority": 150, }, { "code": "splitted_without_availability", "title": "Divididas", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('splitted', '=', True)," - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + "('splitted', '=', True)," + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: r.count_alternative_free_rooms <= 0", "text": f"Parece que a {reservation.partner_name} le ha tocado dormir en habitaciones diferentes " - f" pero no hay ninguna habitación disponible para asignarle, puedes probar a mover otras reservas " - f" para poder establecerle una única habitación. ", + f" pero no hay ninguna habitación disponible para asignarle, puedes probar a mover otras reservas " + f" para poder establecerle una única habitación. ", "priority": 200, }, { "code": "splitted_with_availability", "title": "Divididas", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('splitted', '=', True)," - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + "('splitted', '=', True)," + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: r.count_alternative_free_rooms", "text": f"Parece que a {reservation.partner_name} le ha tocado dormir en habitaciones diferentes" - f" pero tienes la posibilidad de moverlo a {reservation.count_alternative_free_rooms} " - f" {' habitación' if reservation.count_alternative_free_rooms == 1 else ' habitaciones'}.", + f" pero tienes la posibilidad de moverlo a {reservation.count_alternative_free_rooms} " + f" {' habitación' if reservation.count_alternative_free_rooms == 1 else ' habitaciones'}.", "priority": 220, }, { "code": "to_assign", "title": "Por asignar", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('to_assign', '=', True)," - "('reservation_type', 'in', ['normal', 'staff'])," - f"('checkin', '>=', '{today}')," - "]", + "('to_assign', '=', True)," + "('reservation_type', 'in', ['normal', 'staff'])," + f"('checkin', '>=', '{today}')," + "]", "text": f"La reserva de {reservation.partner_name} ha sido asignada a la habitación {reservation.preferred_room_id.name}," - " puedes confirmar la habitación o cambiar a otra desde aquí.", + " puedes confirmar la habitación o cambiar a otra desde aquí.", "priority": 300, }, { "code": "to_confirm", "title": "Por confirmar", "domain": "[('state', '=', 'draft')," - f"('checkin', '>=', '{today}')," - "('reservation_type', 'in', ['normal', 'staff'])," - "]", + f"('checkin', '>=', '{today}')," + "('reservation_type', 'in', ['normal', 'staff'])," + "]", "text": f"La reserva de {reservation.partner_name} está pendiente de confirmar, puedes confirmarla desde aquí.", "priority": 400, }, @@ -1183,36 +1187,36 @@ def wizard_states(self, reservation_id): "code": "checkin_done_precheckin", "title": "Entrada Hoy", "domain": "[('state', '=', 'confirm')," - f"('checkin', '=', '{today}')," - "('pending_checkin_data', '=', 0)," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + f"('checkin', '=', '{today}')," + "('pending_checkin_data', '=', 0)," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "text": "Todos los huéspedes de esta reserva tienen los datos registrados, " - " puedes marcar la entrada directamente desde aquí", + " puedes marcar la entrada directamente desde aquí", "priority": 500, }, { "code": "checkin_partial_precheckin", "title": "Entrada Hoy", "domain": "[('state', '=', 'confirm')," - f"('checkin', '=', '{today}')," - "('pending_checkin_data', '>', 0)," - "('checkin_partner_ids.state','=', 'precheckin')," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + f"('checkin', '=', '{today}')," + "('pending_checkin_data', '>', 0)," + "('checkin_partner_ids.state','=', 'precheckin')," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "text": f"Faltan {reservation.pending_checkin_data} {' huésped ' if reservation.pending_checkin_data == 1 else ' huéspedes '} " - f"por registrar sus datos.Puedes abrir el asistente de checkin " - f" para completar los datos.", + f"por registrar sus datos.Puedes abrir el asistente de checkin " + f" para completar los datos.", "priority": 530, }, { "code": "checkin_no_precheckin", "title": "Entrada Hoy", "domain": "[('state', '=', 'confirm')," - f"('checkin', '=', '{today}')," - "('pending_checkin_data', '>', 0)," - "('reservation_type', 'in', ['normal', 'staff'])" - "]", + f"('checkin', '=', '{today}')," + "('pending_checkin_data', '>', 0)," + "('reservation_type', 'in', ['normal', 'staff'])" + "]", "filtered": "lambda r: all([c.state in ('draft','dummy') for c in r.checkin_partner_ids]) ", "text": "Registra los datos de los huéspedes desde el asistente del checkin.", "priority": 580, @@ -1221,24 +1225,24 @@ def wizard_states(self, reservation_id): "code": "confirmed_without_payment_and_precheckin", "title": "Confirmadas a futuro sin pagar y sin precheckin realizado", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('reservation_type', 'in', ['normal', 'staff'])," - f"('checkin', '>', '{today}')," - "('pending_checkin_data', '>', 0)," - "('folio_payment_state', 'in', ['not_paid', 'partial'])" - "]", + "('reservation_type', 'in', ['normal', 'staff'])," + f"('checkin', '>', '{today}')," + "('pending_checkin_data', '>', 0)," + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", "text": "Esta reserva está pendiente de cobro y de que los huéspedes " - " registren sus datos: puedes enviarles un recordatorio desde aquí", + " registren sus datos: puedes enviarles un recordatorio desde aquí", "priority": 600, }, { "code": "confirmed_without_payment", "title": "Confirmadas a futuro sin pagar", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('reservation_type', 'in', ['normal', 'staff'])," - f"('checkin', '>', '{today}')," - "('pending_checkin_data', '=', 0)," - "('folio_payment_state', 'in', ['not_paid', 'partial'])" - "]", + "('reservation_type', 'in', ['normal', 'staff'])," + f"('checkin', '>', '{today}')," + "('pending_checkin_data', '=', 0)," + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", "text": "Esta reserva está pendiente de cobro, puedes enviarle sun recordatorio desde aquí", "priority": 630, }, @@ -1246,11 +1250,11 @@ def wizard_states(self, reservation_id): "code": "confirmed_without_precheckin", "title": "Confirmadas a futuro sin pagar", "domain": "[('state', 'in', ['draft', 'confirm', 'arrival_delayed'])," - "('reservation_type', 'in', ['normal', 'staff'])," - f"('checkin', '>', '{today}')," - "('pending_checkin_data', '>', 0)," - "('folio_payment_state', 'in', ['paid', 'overpayment','nothing_to_pay'])" - "]", + "('reservation_type', 'in', ['normal', 'staff'])," + f"('checkin', '>', '{today}')," + "('pending_checkin_data', '>', 0)," + "('folio_payment_state', 'in', ['paid', 'overpayment','nothing_to_pay'])" + "]", "text": "Esta reserva no tiene los datos de los huéspedes registrados, puedes enviarles un recordatorio desde aquí", "priority": 660, }, @@ -1258,20 +1262,20 @@ def wizard_states(self, reservation_id): "code": "cancelled", "title": "Cancelada con cargos y sin cobrar", "domain": "[('state', '=', 'cancel')," - "('cancelled_reason', 'in',['late','noshow'])," - "('folio_payment_state', 'in', ['not_paid', 'partial'])," - "]", + "('cancelled_reason', 'in',['late','noshow'])," + "('folio_payment_state', 'in', ['not_paid', 'partial'])," + "]", "filtered": "lambda r: r.service_ids.filtered(lambda s: s.is_cancel_penalty and s.price_total > 0)", "text": f"La reserva de {reservation.partner_name} ha sido cancelada con una penalización de {reservation.service_ids.filtered(lambda s: s.is_cancel_penalty).price_total}€," - " puedes eliminar la penalización en caso de que no se vaya a cobrar.", + " puedes eliminar la penalización en caso de que no se vaya a cobrar.", "priority": 700, }, { "code": "onboard_without_payment", "title": "Por cobrar dentro", "domain": "[('state', 'in', ['onboard', 'departure_delayed'])," - "('folio_payment_state', 'in', ['not_paid', 'partial'])" - "]", + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", "text": f"En esta reserva tenemos un pago pendiente de {reservation.folio_pending_amount}. Puedes registrar el pago desde aquí.", "priority": 800, }, @@ -1279,37 +1283,37 @@ def wizard_states(self, reservation_id): "code": "done_without_payment", "title": "Por cobrar pasadas", "domain": "[('state', '=', 'done')," - "('folio_payment_state', 'in', ['not_paid', 'partial'])" - "]", + "('folio_payment_state', 'in', ['not_paid', 'partial'])" + "]", "text": f"Esta reserva ha quedado con un cargo pendiente de {reservation.folio_pending_amount}€." - " Cuando gestiones el cobro puedes registrarlo desde aquí.", + " Cuando gestiones el cobro puedes registrarlo desde aquí.", "priority": 900, }, { - "code":"checkout", + "code": "checkout", "title": "Checkout", "domain": "[('state', 'in', ['onboard', 'departure_delayed'])," - f"('checkout', '=', '{today}')," - "]", + f"('checkout', '=', '{today}')," + "]", "text": "Reserva lista para el checkout, marca la salida directamente desde aquí.", "priority": 1000, }, ] - # We order the states by priority and return the first state whose domain meets the reservation; - # if the state also has the key 'filtered,' it must also meet that filter. + # We order the states by priority and return the first + # state whose domain meets the reservation; + # if the state also has the key 'filtered,' + # it must also meet that filter. - sorted_wizard_states = sorted(wizard_states, key=lambda x: x['priority']) + sorted_wizard_states = sorted(wizard_states, key=lambda x: x["priority"]) PmsWizardStateInfo = self.env.datamodels["pms.wizard.state.info"] for state in sorted_wizard_states: - domain = expression.AND([ - [("id", "=", reservation_id)], - safe_eval(state["domain"]) - ]) + domain = expression.AND( + [[("id", "=", reservation_id)], safe_eval(state["domain"])] + ) if self.env["pms.reservation"].search_count(domain): - if ( - state.get("filtered") - and not self.env["pms.reservation"].browse(reservation_id).filtered(safe_eval(state["filtered"])) - ): + if state.get("filtered") and not self.env["pms.reservation"].browse( + reservation_id + ).filtered(safe_eval(state["filtered"])): continue return PmsWizardStateInfo( @@ -1319,8 +1323,7 @@ def wizard_states(self, reservation_id): ) return PmsWizardStateInfo( - code='', - title='', - text='', - ) - + code="", + title="", + text="", + ) From e24647a06d74ca48ab369e2cdd223a185f6367b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 17 Apr 2024 11:28:46 +0200 Subject: [PATCH 516/547] [IMP]pms_api_rest: improvement patch invoice service --- pms_api_rest/services/pms_invoice_service.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 11319f8204..1b7a372462 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -228,7 +228,7 @@ def update_invoice(self, invoice_id, pms_invoice_info): if cmd_invoice_lines: new_vals["invoice_line_ids"] = cmd_invoice_lines new_invoice = False - if new_vals and self.check_blocked_fields(invoice, new_vals): + 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 @@ -236,7 +236,9 @@ def update_invoice(self, invoice_id, pms_invoice_info): # 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": + if invoice.state == "posted" and self.check_blocked_fields( + invoice, new_vals + ): # invoice create refund new_invoice = invoice.copy() cmd_new_invoice_lines = [] @@ -263,7 +265,9 @@ def update_invoice(self, invoice_id, pms_invoice_info): new_vals["invoice_line_ids"] = cmd_new_invoice_lines default_values_list = [ { - "ref": _(f'Reversal of: {move.name + " - " + move.ref}'), + "ref": _( + f'Reversal of: {move.name + (" - " + move.ref if move.ref else "")}' + ), } for move in invoice ] @@ -352,7 +356,10 @@ def _direct_move_update(self, invoice, new_vals): def check_blocked_fields(self, invoice, new_vals): # Check partner and amounts - if new_vals.get("partner_id") != invoice.partner_id.id: + 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"]: From 3ebebb0a77a844c261fd6f581d355139f019bfba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 17 Apr 2024 11:29:21 +0200 Subject: [PATCH 517/547] [IMP]pms_api_rest: improvement login token expiration --- pms_api_rest/services/pms_login_service.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/services/pms_login_service.py b/pms_api_rest/services/pms_login_service.py index ccfeaffcf4..782486390e 100644 --- a/pms_api_rest/services/pms_login_service.py +++ b/pms_api_rest/services/pms_login_service.py @@ -37,9 +37,8 @@ def login(self, user): user_record = ( self.env["res.users"].sudo().search([("login", "=", user.username)]) ) - # formula = ms_now + ms in 1 sec * secs in 1 min - minutes = 10000 - timestamp_expire_in_a_min = int(time.time() * 1000.0) + 1000 * 60 * minutes + # 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")) @@ -59,7 +58,7 @@ def login(self, user): { "aud": "api_pms", "iss": "pms", - "exp": timestamp_expire_in_a_min, + "exp": timestamp_expire_in_a_sec, "username": user.username, }, key=validator.secret_key, @@ -71,7 +70,7 @@ def login(self, user): return PmsApiRestUserOutput( token=token, - expirationDate=timestamp_expire_in_a_min, + expirationDate=timestamp_expire_in_a_sec, userId=user_record.id, userName=user_record.name, userFirstName=user_record.firstname or None, From 8bfc7b0fa491615af3362fb404018515ac0aad6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 18 Apr 2024 12:02:18 +0200 Subject: [PATCH 518/547] [IMP]pms_api_rest: improve GET partner documment mapping --- pms_api_rest/services/pms_partner_service.py | 48 ++++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index a43197d0b5..6413cb6095 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -196,19 +196,27 @@ def get_partners(self, pms_partner_search_params): .filtered(lambda x: x.checkout) .mapped("checkout") ) - doc_number = False + 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_number = partner.id_numbers[0] - if doc_number: - if doc_number.name: - document_number = doc_number.name - if doc_number.category_id: - document_type = doc_number.category_id.id - if doc_number.support_number: - document_support_number = doc_number.support_number + 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, @@ -260,7 +268,9 @@ def get_partners(self, pms_partner_search_params): documentSupportNumber=document_support_number if document_support_number else None, - documentCountryId=doc_number.country_id.id if doc_number.country_id else None, + documentCountryId=document_country_id + if document_country_id + else None, vatNumber=partner.vat if partner.vat else partner.aeat_identification @@ -271,10 +281,8 @@ def get_partners(self, pms_partner_search_params): else partner.aeat_identification_type if partner.aeat_identification_type else None, - documentExpeditionDate=datetime.combine( - doc_number.valid_from, datetime.min.time() - ).isoformat() - if doc_number and doc_number.valid_from + 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, @@ -506,7 +514,7 @@ def get_partner_by_doc_number(self, document_type, document_number): ) partners = [] if partner: - doc_number = partner.id_numbers.filtered( + doc_record = partner.id_numbers.filtered( lambda doc: doc.category_id.id == doc_type.id ) PmsCheckinPartnerInfo = self.env.datamodels["pms.checkin.partner.info"] @@ -520,14 +528,14 @@ def get_partner_by_doc_number(self, document_type, document_number): email=partner.email or None, mobile=partner.mobile or None, documentType=doc_type.id or None, - documentNumber=doc_number.name or None, + documentNumber=doc_record.name or None, documentExpeditionDate=datetime.combine( - doc_number.valid_from, datetime.min.time() + doc_record.valid_from, datetime.min.time() ).isoformat() - if doc_number.valid_from + if doc_record.valid_from else None, - documentSupportNumber=doc_number.support_number or None, - documentCountryId=doc_number.country_id.id or 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() From 9120ee6b5c6afc2e145bd957b788c5897cd305bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 25 Apr 2024 19:15:02 +0200 Subject: [PATCH 519/547] [IMP]pms_api_rest: improve and fixe in api logs and transactions logic --- pms_api_rest/services/pms_folio_service.py | 60 +++++++++++++--------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d0269de9ee..151bdca67a 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -764,11 +764,15 @@ def create_folio(self, pms_folio_info): skip_compute_service_ids=False, force_write_blocked=True if external_app else False, )._compute_board_service_room_id() + if reservation.stateCode == "cancel": + reservation.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 # & @@ -840,7 +844,7 @@ def create_folio(self, pms_folio_info): if not external_app: raise ValidationError(_("Error creating folio from API: %s") % e) else: - return False + return 0 def compute_transactions(self, folio, transactions): for transaction in transactions: @@ -849,6 +853,18 @@ def compute_transactions(self, folio, transactions): 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), @@ -856,6 +872,8 @@ def compute_transactions(self, folio, transactions): ("folio_ids", "in", folio.id), ("ref", "ilike", reference), ("state", "=", "posted"), + ("create_uid", "=", self.env.user.id), + ("journal_id", "=", journal.id), ] ) if ( @@ -867,18 +885,6 @@ def compute_transactions(self, folio, transactions): 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, @@ -1116,6 +1122,8 @@ def send_folio_mail(self, folio_id, pms_mail_info): else False, "auto_delete": False, } + if pms_mail_info.bodyMail: + email_values.update({"body": pms_mail_info.bodyMail}) if pms_mail_info.mailType == "confirm": template = folio.pms_property_id.property_confirmed_template res_id = folio.id @@ -1400,7 +1408,7 @@ def _get_section_qty_to_invoice(self, sale_line): lambda l: l.sequence > seq and l.display_type != "line_section" ).mapped("qty_to_invoice") ) - return False + return 0 @restapi.method( [ @@ -1540,15 +1548,17 @@ def get_channel_origin_id(self, sale_channel_id, agency_id): 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"] + 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 @@ -1666,7 +1676,7 @@ def update_put_external_folio(self, external_reference, pms_folio_info): if not external_app: raise ValidationError(_("Error updating folio from API: %s") % e) else: - return False + return 0 @restapi.method( [ @@ -1737,7 +1747,7 @@ def update_put_folio(self, folio_id, pms_folio_info): if not external_app: raise ValidationError(_("Error updating folio from API: %s") % e) else: - return False + return 0 def update_folio_values(self, folio, pms_folio_info): external_app = self.env.user.pms_api_client @@ -1745,7 +1755,7 @@ def update_folio_values(self, folio, pms_folio_info): 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() + draft_invoices.button_cancel() folio.action_cancel() return folio.id # if ( @@ -1898,7 +1908,7 @@ def normalize_payments_structure(self, pms_folio_info, folio): ): journal = ota_conf.pms_api_payment_journal_id pmsTransactionInfo = self.env.datamodels["pms.transaction.info"] - pms_folio_infotransactions = [ + pms_folio_info.transactions = [ pmsTransactionInfo( journalId=journal.id, transactionType="inbound", @@ -2000,9 +2010,13 @@ def wrapper_reservations(self, folio, info_reservations): 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"): + if external_app and info_reservation.boardServiceId: board = self.env["pms.board.service.room.type"].browse( - vals["board_service_room_id"] + 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 += ( From 6576e158e5e30daad4ae926e4b72e42081988729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 12 May 2024 11:13:51 +0200 Subject: [PATCH 520/547] [IMP]pms_api_rest: improvemente compute api transactions and for api push avail in modified reservations requests --- pms_api_rest/services/pms_folio_service.py | 63 ++++++++++++++++++---- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 151bdca67a..dc53b69d9e 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1634,6 +1634,16 @@ def update_put_external_folio(self, external_reference, pms_folio_info): 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, @@ -1704,6 +1714,16 @@ def update_put_folio(self, folio_id, pms_folio_info): 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, @@ -1845,16 +1865,6 @@ def update_folio_values(self, folio, pms_folio_info): ) 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): """ @@ -1892,6 +1902,25 @@ def normalize_payments_structure(self, pms_folio_info, folio): ("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: + amount_total += sum( + [ + line.price - (line.price * ((line.discount or 0.0) * 0.01)) + for line in reservation.reservationLines + if line.price + ] + ) + 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 ( @@ -1912,7 +1941,7 @@ def normalize_payments_structure(self, pms_folio_info, folio): pmsTransactionInfo( journalId=journal.id, transactionType="inbound", - amount=round(folio.amount_total, 2), + amount=round(amount_total, 2), date=fields.Date.today().strftime("%Y-%m-%d"), reference=pms_folio_info.externalReference, ) @@ -2096,6 +2125,18 @@ def wrapper_reservation_lines( }, ) ) + 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): From f07e6f049bf700bfe0c82c2d822f039e4e0bd5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 13 May 2024 11:09:35 +0200 Subject: [PATCH 521/547] [FIX]pms_api_rest: fix compute amount transactions in normalize_payments_structure --- pms_api_rest/services/pms_folio_service.py | 30 ++++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index dc53b69d9e..ffa8fe0616 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1906,20 +1906,22 @@ def normalize_payments_structure(self, pms_folio_info, folio): # and the sum of the price in service lines in the pms_folio_info amount_total = 0 for reservation in pms_folio_info.reservations: - amount_total += sum( - [ - line.price - (line.price * ((line.discount or 0.0) * 0.01)) - for line in reservation.reservationLines - if line.price - ] - ) - amount_total += sum( - [ - service.priceUnit * service.quantity - for service in reservation.services - if service.priceUnit and service.quantity - ] - ) + 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. From 47ee692f0bf851bbdd294bf3503674bc6309c189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 14 May 2024 16:44:00 +0200 Subject: [PATCH 522/547] [FIX]pms_api_rest: fix invoice_ids fields to move_ids --- pms_api_rest/services/pms_folio_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index ffa8fe0616..e4be24d4a6 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1773,7 +1773,7 @@ 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") + draft_invoices = folio.move_ids.filtered(lambda i: i.state == "draft") if draft_invoices: draft_invoices.button_cancel() folio.action_cancel() From 975c8958c8fd6463a3fc85cdca50120372c5b340 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 2 May 2024 15:22:16 +0200 Subject: [PATCH 523/547] [IMP]pms_api_rest: added filter by state overbooking in fetch folios --- pms_api_rest/services/pms_folio_service.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index e4be24d4a6..d820097795 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -105,7 +105,6 @@ def get_folios(self, folio_search_param): 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) @@ -241,6 +240,11 @@ def get_folios(self, folio_search_param): [("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: From 280b24093703fc352320d0f22169cd35324d6cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 16 May 2024 19:42:31 +0200 Subject: [PATCH 524/547] [IMP]pms_api_rest: force_overbooking in PUT external request --- pms_api_rest/services/pms_folio_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index d820097795..4c35dcdb0c 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1845,7 +1845,8 @@ def update_folio_values(self, folio, pms_folio_info): reservations_vals.pop(reservations_vals.index(val)) if val[2].get("state") == "confirm": self.env["pms.reservation"].with_context( - force_write_blocked=True + 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") From 99ba64c33259739bc160e6ccefc3fb4b46631662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 22 May 2024 09:31:43 +0200 Subject: [PATCH 525/547] [IMP]pms_api_rest: fix action_cancel reservation and service name none --- pms_api_rest/services/pms_folio_service.py | 2 +- pms_api_rest/services/pms_reservation_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 4c35dcdb0c..cdf42d3d27 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -769,7 +769,7 @@ def create_folio(self, pms_folio_info): force_write_blocked=True if external_app else False, )._compute_board_service_room_id() if reservation.stateCode == "cancel": - reservation.action_cancel() + reservation_record.action_cancel() pms_folio_info.transactions = self.normalize_payments_structure( pms_folio_info, folio ) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index f6c771270a..4bc7ee519d 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -483,7 +483,7 @@ def get_reservation_services(self, reservation_id): PmsServiceInfo( id=service.id, reservationId=service.reservation_id, - name=service.name, + name=service.name or service.product_id.name, productId=service.product_id.id, quantity=service.product_qty, priceTotal=round(service.price_total, 2), From a33d9c26f1d6d6cd7ef67b678bfba3e07ca60895 Mon Sep 17 00:00:00 2001 From: braisab Date: Fri, 24 May 2024 17:39:03 +0200 Subject: [PATCH 526/547] [IMP]pms_api_rest: added signature field in checkin partner datamodel & addres and other fields in property datamodel --- .../datamodels/pms_checkin_partner.py | 1 + pms_api_rest/datamodels/pms_property.py | 8 +++ pms_api_rest/services/pms_property_service.py | 57 ++++++++++++++++++- .../services/pms_reservation_service.py | 17 ++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_checkin_partner.py b/pms_api_rest/datamodels/pms_checkin_partner.py index 2b5a558bb0..8f334dfaab 100644 --- a/pms_api_rest/datamodels/pms_checkin_partner.py +++ b/pms_api_rest/datamodels/pms_checkin_partner.py @@ -31,3 +31,4 @@ class PmsCheckinPartnerInfo(Datamodel): 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) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index f723b3c02c..fb1c4e5737 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -31,3 +31,11 @@ class PmsPropertyInfo(Datamodel): 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) + stateName = fields.Integer(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) diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index fa0e770f85..f8ba1489f8 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -1,4 +1,5 @@ import base64 +import re from odoo import fields @@ -35,17 +36,30 @@ def get_properties(self): 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, - stateName=state_name if state_name else None, defaultPricelistId=prop.default_pricelist_id.id, colorOptionConfig=prop.color_option_config, preReservationColor=prop.pre_reservation_color, @@ -65,6 +79,16 @@ def get_properties(self): 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, ) ) return result_properties @@ -88,6 +112,25 @@ def get_property(self, property_id): 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, @@ -104,6 +147,18 @@ def get_property(self, property_id): 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, ) return res diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 4bc7ee519d..9eb54b1928 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -1,4 +1,6 @@ import base64 +import os +import tempfile from datetime import datetime, timedelta from odoo import _, fields @@ -625,6 +627,9 @@ def get_checkin_partners(self, reservation_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 @@ -926,6 +931,18 @@ def mapping_checkin_partner_values( 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}) return vals @restapi.method( From 14e264aad999bd404bb67b9831bda25db6c3eeb4 Mon Sep 17 00:00:00 2001 From: braisab Date: Thu, 11 Apr 2024 19:25:59 +0200 Subject: [PATCH 527/547] [IMP]14.0-pms_api_rest: service OCR supplier generic --- pms_api_rest/datamodels/pms_ocr.py | 6 +- pms_api_rest/datamodels/pms_property.py | 1 + pms_api_rest/models/pms_property.py | 6 + pms_api_rest/services/__init__.py | 1 + .../services/ocr_document_service.py | 147 ++------ pms_api_rest/services/pms_property_service.py | 2 + pms_ocr_regula/datamodels/__init__.py | 1 - pms_ocr_regula/datamodels/pms_property.py | 8 - pms_ocr_regula/models/pms_property.py | 332 +++++++++++++++++- pms_ocr_regula/services/__init__.py | 2 - .../services/pms_property_service.py | 26 -- 11 files changed, 370 insertions(+), 162 deletions(-) rename {pms_ocr_regula => pms_api_rest}/services/ocr_document_service.py (58%) delete mode 100644 pms_ocr_regula/datamodels/__init__.py delete mode 100644 pms_ocr_regula/datamodels/pms_property.py delete mode 100644 pms_ocr_regula/services/__init__.py delete mode 100644 pms_ocr_regula/services/pms_property_service.py diff --git a/pms_api_rest/datamodels/pms_ocr.py b/pms_api_rest/datamodels/pms_ocr.py index 205e6dbb22..5e30cd7007 100644 --- a/pms_api_rest/datamodels/pms_ocr.py +++ b/pms_api_rest/datamodels/pms_ocr.py @@ -5,7 +5,9 @@ class PmsOcrInput(Datamodel): _name = "pms.ocr.input" - imageBase64 = fields.String(required=True, allow_none=False) + 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): @@ -24,3 +26,5 @@ class PmsOcrCheckinResult(Datamodel): 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_property.py b/pms_api_rest/datamodels/pms_property.py index fb1c4e5737..248a19b007 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -39,3 +39,4 @@ class PmsPropertyInfo(Datamodel): ineCategory = fields.String(required=False, allow_none=True) cardexWarning = fields.String(required=False, allow_none=True) companyPrivacyPolicy = fields.String(required=False, allow_none=True) + isUsedOCR = fields.Boolean(required=True, allow_none=False) diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index 384249d113..37a5858390 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -111,6 +111,12 @@ class PmsProperty(models.Model): 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() diff --git a/pms_api_rest/services/__init__.py b/pms_api_rest/services/__init__.py index f280265d15..be5f217582 100644 --- a/pms_api_rest/services/__init__.py +++ b/pms_api_rest/services/__init__.py @@ -44,3 +44,4 @@ from . import pms_user_service from . import pms_dashboard_service from . import feed_post_service +from . import ocr_document_service diff --git a/pms_ocr_regula/services/ocr_document_service.py b/pms_api_rest/services/ocr_document_service.py similarity index 58% rename from pms_ocr_regula/services/ocr_document_service.py rename to pms_api_rest/services/ocr_document_service.py index a11d488131..bb518e0ed0 100644 --- a/pms_ocr_regula/services/ocr_document_service.py +++ b/pms_api_rest/services/ocr_document_service.py @@ -34,130 +34,33 @@ class PmsOcr(Component): output_param=Datamodel("pms.ocr.checkin.result", is_list=False), auth="jwt_api_pms", ) - def process_ocr_document_regula(self, input_param): + 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 + checkin_data_dict = hasattr(self, ocr_find_method_name)( + input_param.imageBase64Front, + input_param.imageBase64Back + ) PmsOcrCheckinResult = self.env.datamodels["pms.ocr.checkin.result"] - pms_ocr_checkin_result = PmsOcrCheckinResult() - ocr_regula_url = ( - self.env["ir.config_parameter"].sudo().get_param("ocr_regula_url") + + 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=checkin_data_dict.get('gender') or None, + documentType=checkin_data_dict.get('document_type') or None, + documentExpeditionDate=checkin_data_dict.get('document_expedition_date') or 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 ) - 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=[input_param.imageBase64] - ) - response = api.process(request) - if response.text and response.text.field_list: - # for elemento in response.text.field_list: - # print("campo: ", elemento.field_name) - # print("valor: ", elemento.value) - # print('-') - 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), - ) - 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 diff --git a/pms_api_rest/services/pms_property_service.py b/pms_api_rest/services/pms_property_service.py index f8ba1489f8..af59acc705 100644 --- a/pms_api_rest/services/pms_property_service.py +++ b/pms_api_rest/services/pms_property_service.py @@ -76,6 +76,7 @@ def get_properties(self): simpleInColor=prop.simple_in_color, simpleFutureColor=prop.simple_future_color, language=prop.lang, + isUsedOCR=True if prop.ocr_checkin_supplier else False, hotelImageUrl=url_image_pms_api_rest( "pms.property", prop.id, "hotel_image_pms_api_rest" ), @@ -159,6 +160,7 @@ def get_property(self, property_id): companyPrivacyPolicy=privacy_policy if pms_property.company_id.privacy_policy else None, + isUsedOCR=True if pms_property.ocr_checkin_supplier else False, ) return res diff --git a/pms_ocr_regula/datamodels/__init__.py b/pms_ocr_regula/datamodels/__init__.py deleted file mode 100644 index 9216f6ffd4..0000000000 --- a/pms_ocr_regula/datamodels/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import pms_property diff --git a/pms_ocr_regula/datamodels/pms_property.py b/pms_ocr_regula/datamodels/pms_property.py deleted file mode 100644 index 15a988cb00..0000000000 --- a/pms_ocr_regula/datamodels/pms_property.py +++ /dev/null @@ -1,8 +0,0 @@ -from marshmallow import fields - -from odoo.addons.datamodel.core import Datamodel - - -class PmsPropertyInfo(Datamodel): - _inherit = "pms.property.info" - isUsedRegula = fields.Boolean(required=False, allow_none=True) diff --git a/pms_ocr_regula/models/pms_property.py b/pms_ocr_regula/models/pms_property.py index c5530fcae2..abd24850a1 100644 --- a/pms_ocr_regula/models/pms_property.py +++ b/pms_ocr_regula/models/pms_property.py @@ -1,9 +1,337 @@ +from regula.documentreader.webclient import ( + DocumentReaderApi, + ProcessParams, + RecognitionRequest, + Result, + Scenario, + TextFieldType, +) +from datetime import date, datetime +from dateutil.relativedelta import relativedelta + from odoo import fields, models + class PmsProperty(models.Model): _inherit = "pms.property" - is_used_regula = fields.Boolean( - string="Used regula", help="True if this property uses regula's OCR" + 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/services/__init__.py b/pms_ocr_regula/services/__init__.py deleted file mode 100644 index 918dbaad52..0000000000 --- a/pms_ocr_regula/services/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import ocr_document_service -from . import pms_property_service diff --git a/pms_ocr_regula/services/pms_property_service.py b/pms_ocr_regula/services/pms_property_service.py deleted file mode 100644 index 1e633dce89..0000000000 --- a/pms_ocr_regula/services/pms_property_service.py +++ /dev/null @@ -1,26 +0,0 @@ -from odoo.addons.base_rest import restapi -from odoo.addons.base_rest_datamodel.restapi import Datamodel -from odoo.addons.component.core import Component - - -class PmsPropertyService(Component): - _inherit = "pms.property.service" - - @restapi.method( - [ - ( - [ - "/", - ], - "GET", - ) - ], - output_param=Datamodel("pms.property.info", is_list=True), - auth="jwt_api_pms", - ) - def get_properties(self): - result_properties = super(PmsPropertyService, self).get_properties() - for prop_info in result_properties: - pms_property = self.env["pms.property"].browse(prop_info.id) - prop_info.isUsedRegula = pms_property.is_used_regula - return result_properties From 637c8636335293473e627a6e548541526dbe8a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 22 Apr 2024 12:58:37 +0200 Subject: [PATCH 528/547] [ADD]pms_ocr_klippa: ADD klippa OCR basic flow --- pms_api_rest/services/ocr_document_service.py | 259 ++--------- pms_api_rest/views/pms_property_views.xml | 5 +- pms_api_rest/views/res_users_views.xml | 2 +- pms_ocr_klippa/README.rst | 81 ++++ pms_ocr_klippa/__init__.py | 1 + pms_ocr_klippa/__manifest__.py | 20 + pms_ocr_klippa/data/pms_ocr_klippa_data.xml | 32 ++ pms_ocr_klippa/models/__init__.py | 2 + pms_ocr_klippa/models/pms_property.py | 187 ++++++++ .../models/res_partner_id_category.py | 12 + pms_ocr_klippa/readme/CONTRIBUTORS.rst | 1 + pms_ocr_klippa/readme/DESCRIPTION.rst | 1 + pms_ocr_klippa/readme/USAGE.rst | 1 + pms_ocr_klippa/static/description/index.html | 426 ++++++++++++++++++ .../views/res_partner_id_category_views.xml | 16 + pms_ocr_regula/README.rst | 4 +- pms_ocr_regula/__init__.py | 2 - pms_ocr_regula/__manifest__.py | 2 +- pms_ocr_regula/models/pms_property.py | 42 +- pms_ocr_regula/readme/USAGE.rst | 2 +- pms_ocr_regula/static/description/index.html | 4 +- pms_ocr_regula/views/pms_property_views.xml | 14 - .../pms_ocr_klippa/odoo/addons/pms_ocr_klippa | 1 + setup/pms_ocr_klippa/setup.py | 6 + 24 files changed, 850 insertions(+), 273 deletions(-) create mode 100644 pms_ocr_klippa/README.rst create mode 100644 pms_ocr_klippa/__init__.py create mode 100644 pms_ocr_klippa/__manifest__.py create mode 100644 pms_ocr_klippa/data/pms_ocr_klippa_data.xml create mode 100644 pms_ocr_klippa/models/__init__.py create mode 100644 pms_ocr_klippa/models/pms_property.py create mode 100644 pms_ocr_klippa/models/res_partner_id_category.py create mode 100644 pms_ocr_klippa/readme/CONTRIBUTORS.rst create mode 100644 pms_ocr_klippa/readme/DESCRIPTION.rst create mode 100644 pms_ocr_klippa/readme/USAGE.rst create mode 100644 pms_ocr_klippa/static/description/index.html create mode 100644 pms_ocr_klippa/views/res_partner_id_category_views.xml delete mode 100644 pms_ocr_regula/views/pms_property_views.xml create mode 120000 setup/pms_ocr_klippa/odoo/addons/pms_ocr_klippa create mode 100644 setup/pms_ocr_klippa/setup.py diff --git a/pms_api_rest/services/ocr_document_service.py b/pms_api_rest/services/ocr_document_service.py index bb518e0ed0..1d1424888f 100644 --- a/pms_api_rest/services/ocr_document_service.py +++ b/pms_api_rest/services/ocr_document_service.py @@ -1,14 +1,4 @@ -from datetime import date, datetime - -from dateutil.relativedelta import relativedelta -from regula.documentreader.webclient import ( - DocumentReaderApi, - ProcessParams, - RecognitionRequest, - Result, - Scenario, - TextFieldType, -) +from datetime import datetime from odoo.addons.base_rest import restapi from odoo.addons.base_rest_datamodel.restapi import Datamodel @@ -35,225 +25,40 @@ class PmsOcr(Component): 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 - checkin_data_dict = hasattr(self, ocr_find_method_name)( - input_param.imageBase64Front, - input_param.imageBase64Back + 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=checkin_data_dict.get('gender') or None, - documentType=checkin_data_dict.get('document_type') or None, - documentExpeditionDate=checkin_data_dict.get('document_expedition_date') or 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 - ) - - 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)] + 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" ) - elif nationality_code and nationality_code.value != "": - country = self.env["res.country"].search( - [("code_alpha3", "=", nationality_code.value)] + 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" ) - 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 + 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/views/pms_property_views.xml b/pms_api_rest/views/pms_property_views.xml index e0b5286157..f419ad45ed 100644 --- a/pms_api_rest/views/pms_property_views.xml +++ b/pms_api_rest/views/pms_property_views.xml @@ -91,7 +91,10 @@ - + + + + diff --git a/pms_api_rest/views/res_users_views.xml b/pms_api_rest/views/res_users_views.xml index c639349d58..39570f6b94 100644 --- a/pms_api_rest/views/res_users_views.xml +++ b/pms_api_rest/views/res_users_views.xml @@ -18,7 +18,7 @@ domain="['&',('model_id', '=', 'pms.availability.plan.rule'), ('name', 'in', ('min_stay', 'max_stay', 'quota', 'max_stay_arrival', 'closed_arrival', 'closed', 'closed_departure', 'min_stay_arrival', 'max_avail'))]" /> - + `_. +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..58d22ddd1f --- /dev/null +++ b/pms_ocr_klippa/__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 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", + ], + "data": [ + "data/pms_ocr_klippa_data.xml", + "views/res_partner_id_category_views.xml", + ], + "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..51274094d6 --- /dev/null +++ b/pms_ocr_klippa/data/pms_ocr_klippa_data.xml @@ -0,0 +1,32 @@ + + + + + ocr_klippa_api_key + False + + + ocr_klippa_url + https://custom-ocr.klippa.com/api/v1/parseDocument/identity + + + + + P + P + + + I + I + + + I + D + + + diff --git a/pms_ocr_klippa/models/__init__.py b/pms_ocr_klippa/models/__init__.py new file mode 100644 index 0000000000..45a7df3f43 --- /dev/null +++ b/pms_ocr_klippa/models/__init__.py @@ -0,0 +1,2 @@ +from . import pms_property +from . import res_partner_id_category diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py new file mode 100644 index 0000000000..c1b9503db7 --- /dev/null +++ b/pms_ocr_klippa/models/pms_property.py @@ -0,0 +1,187 @@ +from datetime import date, datetime + +import requests +from dateutil.relativedelta import relativedelta + +from odoo import _, fields, models +from odoo.exceptions import ValidationError + + +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): + 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_back: + 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, + } + + # Call Klippa OCR API + result = requests.post( + ocr_klippa_url, + headers=headers, + json=payload, + ) + json_data = result.json() + if json_data.get("result") != "success": + raise ValidationError(_("Error calling Klippa OCR API")) + document_data = json_data["data"]["parsed"] + mapped_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: + if "street_name" in value: + mapped_data["residence_street"] = value["street_name"] + ( + " " + value["house_number"] if "house_number" in value else "" + ) + if "city" in value: + mapped_data["residence_city"] = value["city"] + if "postcode" in value: + mapped_data["zip"] = value["postcode"] + if "province" in value: + mapped_data["residence_state_id"] = ( + self.env["res.country.state"] + .search( + [ + ("name", "ilike", value["province"]), + ( + "country_id", + "=", + self._get_country_id(value.get("country", False)), + ), + ] + ) + .id + or False + ) + + # Document Data -------------------------------------------------- + elif key == "issuing_country" and value: + mapped_data["document_country_id"] = self._get_country_id(value) + elif key == "document_number" and value: + mapped_data["document_support_number"] = 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", False), + ) + elif key == "personal_number" and value: + mapped_data["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_expiration_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", 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) + return mapped_data + + def _calc_expedition_date(self, document_type, 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-%dT%H:%M:%S" + ).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-%dT%H:%M:%S" + ).date() + if person_age < 30: + result = date_of_expiry - relativedelta(years=5) + elif person_age >= 30 and document_type and document_type.code == "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 _get_document_type(self, klippa_type, klippa_subtype): + document_type_ids = self.env["res.partner.id_category"].search( + [ + ("klippa_code", "=", klippa_type), + ] + ) + if not document_type_ids: + raise ValidationError(_(f"Document type not found: {klippa_type}")) + document_type_id = document_type_ids[0] + if len(document_type_ids) > 1: + document_type_id = document_type_ids.filtered( + lambda r: r.klippa_subtype_code == klippa_subtype + ).id + return document_type_id + + def _get_country_id(self, country_code): + 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 + if " " in origin_surname: + return origin_surname.split(" ") + else: + return [origin_surname, ""] 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/static/description/index.html b/pms_ocr_klippa/static/description/index.html new file mode 100644 index 0000000000..959844b8be --- /dev/null +++ b/pms_ocr_klippa/static/description/index.html @@ -0,0 +1,426 @@ + + + + + + +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/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 index 7f2338b2ea..f9fe1e2f95 100644 --- a/pms_ocr_regula/README.rst +++ b/pms_ocr_regula/README.rst @@ -7,7 +7,7 @@ OCR Regula !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:b34369f690039d9865de6496bc7fd3d815f16fb385b83e1e7d3db0e35ebabeb7 + !! source digest: sha256:4db37aab9c7f834aaf48397c989242dd06463f3a2a4b652d4d7dc2def9584db4 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -38,7 +38,7 @@ Module to connect the OCR regula with the pms Usage ===== -Set api key regula and url parameters of the OCR service and activate the is_used_regula field in pms_property +Set api key klippa and url parameters of the OCR service and select regula provider ocr in pms_property Bug Tracker =========== diff --git a/pms_ocr_regula/__init__.py b/pms_ocr_regula/__init__.py index c82439595a..0650744f6b 100644 --- a/pms_ocr_regula/__init__.py +++ b/pms_ocr_regula/__init__.py @@ -1,3 +1 @@ from . import models -from . import services -from . import datamodels diff --git a/pms_ocr_regula/__manifest__.py b/pms_ocr_regula/__manifest__.py index 6bf93f99ec..2bb0bbed0c 100644 --- a/pms_ocr_regula/__manifest__.py +++ b/pms_ocr_regula/__manifest__.py @@ -15,6 +15,6 @@ "external_dependencies": { "python": ["regula.documentreader.webclient", "marshmallow"], }, - "data": ["views/pms_property_views.xml", "data/pms_ocr_regula_data.xml"], + "data": ["data/pms_ocr_regula_data.xml"], "installable": True, } diff --git a/pms_ocr_regula/models/pms_property.py b/pms_ocr_regula/models/pms_property.py index abd24850a1..340d0d0e51 100644 --- a/pms_ocr_regula/models/pms_property.py +++ b/pms_ocr_regula/models/pms_property.py @@ -1,3 +1,6 @@ +from datetime import date, datetime + +from dateutil.relativedelta import relativedelta from regula.documentreader.webclient import ( DocumentReaderApi, ProcessParams, @@ -6,19 +9,14 @@ Scenario, TextFieldType, ) -from datetime import date, datetime -from dateutil.relativedelta import relativedelta from odoo import fields, models - class PmsProperty(models.Model): _inherit = "pms.property" - ocr_checkin_supplier = fields.Selection( - selection_add=["regula", "Regula"] - ) + 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 = ( @@ -58,16 +56,16 @@ def _regula_document_process(self, image_base_64_front, image_base_64_back=False ) pms_ocr_checkin_result = dict() if country_id: - pms_ocr_checkin_result['nationality'] = country_id + pms_ocr_checkin_result["nationality"] = country_id if firstname: - pms_ocr_checkin_result['firstname'] = firstname + pms_ocr_checkin_result["firstname"] = firstname if lastname: - pms_ocr_checkin_result['lastname'] = lastname + pms_ocr_checkin_result["lastname"] = lastname if lastname2: - pms_ocr_checkin_result['lastname2'] = lastname2 + pms_ocr_checkin_result["lastname2"] = lastname2 gender = response.text.get_field(TextFieldType.SEX) if gender and gender.value != "": - pms_ocr_checkin_result['gender'] = ( + pms_ocr_checkin_result["gender"] = ( "male" if gender.value == "M" else "female" @@ -76,7 +74,7 @@ def _regula_document_process(self, image_base_64_front, image_base_64_back=False ) 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'] = ( + pms_ocr_checkin_result["birthdate"] = ( datetime.strptime( date_of_birth.value.replace("-", "/"), "%Y/%m/%d" ) @@ -93,7 +91,7 @@ def _regula_document_process(self, image_base_64_front, image_base_64_back=False and document_class_code.value != "" and document_class_code.value == "P" ): - pms_ocr_checkin_result['documentType'] = ( + pms_ocr_checkin_result["documentType"] = ( self.env["res.partner.id_category"] .search([("code", "=", "P")]) .id @@ -108,11 +106,11 @@ def _regula_document_process(self, image_base_64_front, image_base_64_back=False age, date_of_birth, ) - pms_ocr_checkin_result['documentExpeditionDate'] = date_of_issue + 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("-", "/") - ) + pms_ocr_checkin_result[ + "documentExpeditionDate" + ] = date_of_issue.value.replace("-", "/") support_number, document_number = self._proccess_document_number( id_country_spain, country_id, @@ -121,9 +119,9 @@ def _regula_document_process(self, image_base_64_front, image_base_64_back=False response.text.get_field(TextFieldType.PERSONAL_NUMBER), ) if support_number: - pms_ocr_checkin_result['documentSupportNumber'] = support_number + pms_ocr_checkin_result["documentSupportNumber"] = support_number if document_number: - pms_ocr_checkin_result['documentNumber'] = document_number + pms_ocr_checkin_result["documentNumber"] = document_number address_street, address_city, address_area = self._process_address( id_country_spain, country_id, @@ -133,11 +131,11 @@ def _regula_document_process(self, image_base_64_front, image_base_64_back=False response.text.get_field(TextFieldType.ADDRESS), ) if address_street: - pms_ocr_checkin_result['residenceStreet'] = address_street + pms_ocr_checkin_result["residenceStreet"] = address_street if address_city: - pms_ocr_checkin_result['residenceCity'] = address_city + pms_ocr_checkin_result["residenceCity"] = address_city if address_area: - pms_ocr_checkin_result['countryState'] = address_area + pms_ocr_checkin_result["countryState"] = address_area return pms_ocr_checkin_result def _process_nationality( diff --git a/pms_ocr_regula/readme/USAGE.rst b/pms_ocr_regula/readme/USAGE.rst index d74b8cd5d8..4da449a3cd 100644 --- a/pms_ocr_regula/readme/USAGE.rst +++ b/pms_ocr_regula/readme/USAGE.rst @@ -1 +1 @@ -Set api key regula and url parameters of the OCR service and activate the is_used_regula field in pms_property +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 index 77909fefbf..98ac023e20 100644 --- a/pms_ocr_regula/static/description/index.html +++ b/pms_ocr_regula/static/description/index.html @@ -367,7 +367,7 @@

OCR Regula

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:b34369f690039d9865de6496bc7fd3d815f16fb385b83e1e7d3db0e35ebabeb7 +!! source digest: sha256:4db37aab9c7f834aaf48397c989242dd06463f3a2a4b652d4d7dc2def9584db4 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Module to connect the OCR regula with the pms

@@ -386,7 +386,7 @@

OCR Regula

Usage

-

Set api key regula and url parameters of the OCR service and activate the is_used_regula field in pms_property

+

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

Bug Tracker

diff --git a/pms_ocr_regula/views/pms_property_views.xml b/pms_ocr_regula/views/pms_property_views.xml deleted file mode 100644 index b7776c60b2..0000000000 --- a/pms_ocr_regula/views/pms_property_views.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - pms.property - - - - - - - - - - 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, +) From 598ccb8828e6238766a2c3f99b185457e8ae6d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sat, 4 May 2024 15:24:35 +0200 Subject: [PATCH 529/547] [ADD] nominatim address calls --- pms_ocr_klippa/README.rst | 2 +- pms_ocr_klippa/__manifest__.py | 1 + pms_ocr_klippa/models/pms_property.py | 180 ++++++++++++++----- pms_ocr_klippa/static/description/index.html | 13 +- requirements.txt | 2 + 5 files changed, 151 insertions(+), 47 deletions(-) diff --git a/pms_ocr_klippa/README.rst b/pms_ocr_klippa/README.rst index 91e4fd4f3c..aa976009d7 100644 --- a/pms_ocr_klippa/README.rst +++ b/pms_ocr_klippa/README.rst @@ -7,7 +7,7 @@ OCR Klippa !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:0de4876412fe017db56d0ae207c4aa1f4f01394f57851ae281b5d94f5fc20c5f + !! source digest: sha256:2b9fc0252a9368c795df1d59126287bd5538d1a23498d99bfb72658e7a2a6eff !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/pms_ocr_klippa/__manifest__.py b/pms_ocr_klippa/__manifest__.py index 58d22ddd1f..f28b501b7b 100644 --- a/pms_ocr_klippa/__manifest__.py +++ b/pms_ocr_klippa/__manifest__.py @@ -12,6 +12,7 @@ "depends": [ "pms_api_rest", ], + "external_dependencies": {"python": ["thefuzz", "geopy"]}, "data": [ "data/pms_ocr_klippa_data.xml", "views/res_partner_id_category_views.xml", diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index c1b9503db7..5a16788ee0 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -1,11 +1,16 @@ +import logging from datetime import date, datetime import requests from dateutil.relativedelta import relativedelta +from geopy.geocoders import Nominatim +from thefuzz import process from odoo import _, fields, models from odoo.exceptions import ValidationError +_logger = logging.getLogger(__name__) + class PmsProperty(models.Model): _inherit = "pms.property" @@ -21,7 +26,7 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False self.env["ir.config_parameter"].sudo().get_param("ocr_klippa_api_key") ) document = [] - if image_base_64_back: + if image_base_64_front: document.append(image_base_64_front) if image_base_64_back: document.append(image_base_64_back) @@ -54,30 +59,7 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False continue # Residence Address -------------------------------------------------- if key == "address" and value: - if "street_name" in value: - mapped_data["residence_street"] = value["street_name"] + ( - " " + value["house_number"] if "house_number" in value else "" - ) - if "city" in value: - mapped_data["residence_city"] = value["city"] - if "postcode" in value: - mapped_data["zip"] = value["postcode"] - if "province" in value: - mapped_data["residence_state_id"] = ( - self.env["res.country.state"] - .search( - [ - ("name", "ilike", value["province"]), - ( - "country_id", - "=", - self._get_country_id(value.get("country", False)), - ), - ] - ) - .id - or False - ) + mapped_data = self._complete_residence_address(value, mapped_data) # Document Data -------------------------------------------------- elif key == "issuing_country" and value: @@ -87,7 +69,9 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False elif key == "document_type" and value: mapped_data["document_type"] = self._get_document_type( klippa_type=value, - klippa_subtype=document_data.get("document_subtype", False), + country_id=self._get_country_id( + document_data.get("document_country_id", False) + ), ) elif key == "personal_number" and value: mapped_data["document_number"] = value @@ -100,10 +84,12 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False and value and not document_data.get("date_of_issue", False) ): - mapped_data["document_expiration_date"] = self._calc_expedition_date( + 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", False), + country_id=self._get_country_id( + document_data.get("document_country_id", False) + ), ), date_of_expiry=value, age=False, @@ -135,29 +121,36 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False mapped_data["nationality"] = self._get_country_id(value) return mapped_data - def _calc_expedition_date(self, document_type, date_of_expiry, age, date_of_birth): + 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 != "": + if age: + person_age = age + elif date_of_birth and date_of_birth.get("value") != "": date_of_birth = datetime.strptime( - date_of_birth.value.replace("-", "/"), "%Y-%m-%dT%H:%M:%S" + 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.value != "" and person_age: + if date_of_expiry and date_of_expiry != "" and person_age: date_of_expiry = datetime.strptime( - date_of_expiry.value.replace("-", "/"), "%Y-%m-%dT%H:%M:%S" + 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_type and document_type.code == "P": + 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.isoformat() if result else False + return result if result else False - def _get_document_type(self, klippa_type, klippa_subtype): + def _get_document_type(self, klippa_type, country_id): + document_type_id = False document_type_ids = self.env["res.partner.id_category"].search( [ ("klippa_code", "=", klippa_type), @@ -165,11 +158,15 @@ def _get_document_type(self, klippa_type, klippa_subtype): ) if not document_type_ids: raise ValidationError(_(f"Document type not found: {klippa_type}")) - document_type_id = document_type_ids[0] + if len(document_type_ids) > 1: document_type_id = document_type_ids.filtered( - lambda r: r.klippa_subtype_code == klippa_subtype - ).id + lambda r: country_id in r.country_ids.ids + ) + if not document_type_id: + document_type_id = document_type_ids.filtered(lambda r: not r.country_ids)[ + 0 + ] return document_type_id def _get_country_id(self, country_code): @@ -185,3 +182,104 @@ def _get_surnames(self, origin_surname): return origin_surname.split(" ") else: return [origin_surname, ""] + + 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])] + ) + mapped_data["residence_state_id"] = country_state.id + if not country_record: + mapped_data["country_id"] = country_state.country_id.id + else: + mapped_data["residence_state_id"] = None + 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"] + + 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()): + geolocator = Nominatim(user_agent="roomdoo_pms") + search_address_str = f"{street_name}, {mapped_data.get('residence_city', '')}, {mapped_data.get('zip', '')}, {mapped_data.get('country_id', '')}" + location = geolocator.geocode( + search_address_str, + addressdetails=True, + timeout=5, + language="en", + ) + if not location: + search_address_str = f"{mapped_data.get('residence_city', '')}, {mapped_data.get('zip', '')}, {mapped_data.get('country_id', '')}" + location = geolocator.geocode( + search_address_str, + addressdetails=True, + timeout=5, + language="en", + ) + if location: + if not mapped_data.get("zip", False): + mapped_data["zip"] = location.raw.get("address", {}).get( + "postcode", False + ) + if not mapped_data.get("country_id", False): + country_match_name = process.extractOne( + location.raw.get("address", {}).get("country", False), + self.env["res.country"].search([]).mapped("name"), + ) + if country_match_name[1] >= 90: + country_record = self.env["res.country"].search( + [("name", "=", country_match_name[0])] + ) + mapped_data["country_id"] = country_record.id + if not mapped_data.get("country_state", False): + country_state_record = process.extractOne( + location.raw.get("address", {}).get("province", False), + self.env["res.country.state"].search([]).mapped("name"), + ) + if country_state_record[1] >= 90: + country_state = self.env["res.country.state"].search( + [("name", "=", country_state_record[0])] + ) + mapped_data["country_state"] = country_state.id + if not mapped_data.get("residence_city", False): + mapped_data["residence_city"] = location.raw.get("address", {}).get( + "city", False + ) + if not mapped_data.get("residence_street", False): + mapped_data["residence_street"] = location.raw.get( + "address", {} + ).get("road", False) + return mapped_data diff --git a/pms_ocr_klippa/static/description/index.html b/pms_ocr_klippa/static/description/index.html index 959844b8be..aa53b2c3c1 100644 --- a/pms_ocr_klippa/static/description/index.html +++ b/pms_ocr_klippa/static/description/index.html @@ -9,10 +9,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +276,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +302,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -367,7 +368,7 @@

OCR Klippa

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:0de4876412fe017db56d0ae207c4aa1f4f01394f57851ae281b5d94f5fc20c5f +!! source digest: sha256:2b9fc0252a9368c795df1d59126287bd5538d1a23498d99bfb72658e7a2a6eff !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Module to connect the OCR Klippa with the pms

@@ -413,7 +414,9 @@

Contributors

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +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.

diff --git a/requirements.txt b/requirements.txt index aa2e5d60ac..fb1fad6751 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,11 @@ # generated from manifests external_dependencies bs4 +geopy jose jwt marshmallow pycountry regula.documentreader.webclient simplejson +thefuzz xlrd From 226fe5ffb4bf37d279205307e3b8c062ddddb47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 12 May 2024 21:13:14 +0200 Subject: [PATCH 530/547] [IMP]pms_ocr_klippa: improvement address ocr heuristics --- pms_ocr_klippa/models/pms_property.py | 89 +++++++++++++++++++++------ 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index 5a16788ee0..d5a9dddbf6 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -70,7 +70,9 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False mapped_data["document_type"] = self._get_document_type( klippa_type=value, country_id=self._get_country_id( - document_data.get("document_country_id", False) + document_data.get("issuing_country").get("value") + if document_data.get("issuing_country") + else False ), ) elif key == "personal_number" and value: @@ -88,7 +90,9 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False document_class_code=self._get_document_type( klippa_type=document_data.get("document_class_code", False), country_id=self._get_country_id( - document_data.get("document_country_id", False) + document_data.get("issuing_country").get("value") + if document_data.get("issuing_country") + else False ), ), date_of_expiry=value, @@ -222,6 +226,27 @@ def _complete_residence_address(self, value, mapped_data): 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["residence_state_id"] = ( + zip_code.city_id.state_id.id + if not mapped_data.get("residence_state_id", False) + else mapped_data["residence_state_id"] + ) + 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, @@ -242,38 +267,64 @@ def _complete_residence_address(self, value, mapped_data): language="en", ) if not location: - search_address_str = f"{mapped_data.get('residence_city', '')}, {mapped_data.get('zip', '')}, {mapped_data.get('country_id', '')}" - location = geolocator.geocode( - search_address_str, - addressdetails=True, - timeout=5, - language="en", - ) + street_words = street_name.split(" ") + street_words = [word for word in street_words if len(word) > 2] + while street_words and not location: + street_name = " ".join(street_words) + search_address_str = f"{street_name}, {mapped_data.get('residence_city', '')}, {mapped_data.get('zip', '')}, {mapped_data.get('country_id', '')}" + location = geolocator.geocode( + search_address_str, + addressdetails=True, + timeout=5, + language="en", + ) + street_words.pop(0) if location: if not mapped_data.get("zip", False): mapped_data["zip"] = location.raw.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_match_name = process.extractOne( location.raw.get("address", {}).get("country", False), - self.env["res.country"].search([]).mapped("name"), + self.env["res.country"] + .with_context(lang="en_US") + .search([]) + .mapped("name"), ) if country_match_name[1] >= 90: - country_record = self.env["res.country"].search( - [("name", "=", country_match_name[0])] + country_record = ( + self.env["res.country"] + .with_context(lang="en_US") + .search([("name", "=", country_match_name[0])]) ) mapped_data["country_id"] = country_record.id if not mapped_data.get("country_state", False): - country_state_record = process.extractOne( - location.raw.get("address", {}).get("province", False), - self.env["res.country.state"].search([]).mapped("name"), + state_name = ( + location.raw.get("address", {}).get("prorvince") + if location.raw.get("address", {}).get("province") + else location.raw.get("address", {}).get("state") ) - if country_state_record[1] >= 90: - country_state = self.env["res.country.state"].search( - [("name", "=", country_state_record[0])] + if state_name: + country_state_record = process.extractOne( + state_name, + self.env["res.country.state"].search([]).mapped("name"), ) - mapped_data["country_state"] = country_state.id + if country_state_record[1] >= 90: + country_state = self.env["res.country.state"].search( + [("name", "=", country_state_record[0])] + ) + mapped_data["country_state"] = country_state.id if not mapped_data.get("residence_city", False): mapped_data["residence_city"] = location.raw.get("address", {}).get( "city", False From b38fce9ef9a43ccbbfdff0a17c133dac0a4849e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 13 May 2024 19:41:07 +0200 Subject: [PATCH 531/547] [IMP]pms_ocr_klippa:change location by requests consult --- pms_ocr_klippa/models/pms_property.py | 131 +++++++++++++++++--------- 1 file changed, 85 insertions(+), 46 deletions(-) diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index d5a9dddbf6..cf80bbed40 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -3,7 +3,6 @@ import requests from dateutil.relativedelta import relativedelta -from geopy.geocoders import Nominatim from thefuzz import process from odoo import _, fields, models @@ -11,6 +10,8 @@ _logger = logging.getLogger(__name__) +NOMINATIM_URL = "https://nominatim.openstreetmap.org/search" + class PmsProperty(models.Model): _inherit = "pms.property" @@ -52,6 +53,18 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False raise ValidationError(_("Error calling Klippa OCR API")) document_data = json_data["data"]["parsed"] mapped_data = {} + found_partner = False + if document_data.get("personal_number", False): + found_partner = ( + self.env["res.partner.id_number"] + .search( + [ + ("name", "=", document_data["personal_number"]["value"]), + ], + limit=1, + ) + .partner_id + ) for key, dict_value in document_data.items(): if dict_value and isinstance(dict_value, dict): value = dict_value.get("value", False) @@ -168,10 +181,8 @@ def _get_document_type(self, klippa_type, country_id): lambda r: country_id in r.country_ids.ids ) if not document_type_id: - document_type_id = document_type_ids.filtered(lambda r: not r.country_ids)[ - 0 - ] - return document_type_id + document_type_id = document_type_ids[0] + return document_type_id[0] def _get_country_id(self, country_code): return ( @@ -217,10 +228,10 @@ def _complete_residence_address(self, value, mapped_data): domain + [("name", "=", candidates[0])] ) mapped_data["residence_state_id"] = country_state.id - if not country_record: + if not country_record and country_state: mapped_data["country_id"] = country_state.country_id.id else: - mapped_data["residence_state_id"] = None + mapped_data["residence_state_id"] = 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 @@ -258,30 +269,44 @@ def _complete_residence_address(self, value, mapped_data): # 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()): - geolocator = Nominatim(user_agent="roomdoo_pms") - search_address_str = f"{street_name}, {mapped_data.get('residence_city', '')}, {mapped_data.get('zip', '')}, {mapped_data.get('country_id', '')}" - location = geolocator.geocode( - search_address_str, - addressdetails=True, - timeout=5, - language="en", - ) - if not location: + 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"] + if street_name: + # Clean street name with mains words street_words = street_name.split(" ") - street_words = [word for word in street_words if len(word) > 2] - while street_words and not location: - street_name = " ".join(street_words) - search_address_str = f"{street_name}, {mapped_data.get('residence_city', '')}, {mapped_data.get('zip', '')}, {mapped_data.get('country_id', '')}" - location = geolocator.geocode( - search_address_str, - addressdetails=True, - timeout=5, - language="en", - ) - street_words.pop(0) - if location: + params["street"] = " ".join( + [word for word in street_words if len(word) > 2] + ) + location = requests.get(NOMINATIM_URL, params=params) + if not location.json() or location.status_code != 200: + # If not found address, pop the street to try again + if street_name: + params.pop("street") + location = requests.get(NOMINATIM_URL, params=params) + if location.json() and location.status_code == 200: + location = location.json()[0] + _logger.info(location) if not mapped_data.get("zip", False): - mapped_data["zip"] = location.raw.get("address", {}).get( + mapped_data["zip"] = location.get("address", {}).get( "postcode", False ) if mapped_data["zip"]: @@ -295,25 +320,39 @@ def _complete_residence_address(self, value, mapped_data): "country_id" ] = zip_code.city_id.state_id.country_id.id if not mapped_data.get("country_id", False): - country_match_name = process.extractOne( - location.raw.get("address", {}).get("country", False), - self.env["res.country"] - .with_context(lang="en_US") - .search([]) - .mapped("name"), + country_record = self.env["res.country"].search( + [ + ( + "code", + "=", + location.get("address", {}) + .get("country_code", False) + .upper(), + ) + ] ) - if country_match_name[1] >= 90: - country_record = ( + 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([("name", "=", country_match_name[0])]) + .search([]) + .mapped("name"), ) - mapped_data["country_id"] = country_record.id + if country_match[1] >= 90: + country_record = ( + self.env["res.country"] + .with_context(lang="en_US") + .search([("name", "=", country_match_name[0])]) + ) + mapped_data["country_id"] = country_record.id if not mapped_data.get("country_state", False): state_name = ( - location.raw.get("address", {}).get("prorvince") - if location.raw.get("address", {}).get("province") - else location.raw.get("address", {}).get("state") + location.get("address", {}).get("province") + if location.get("address", {}).get("province") + else location.get("address", {}).get("state") ) if state_name: country_state_record = process.extractOne( @@ -326,11 +365,11 @@ def _complete_residence_address(self, value, mapped_data): ) mapped_data["country_state"] = country_state.id if not mapped_data.get("residence_city", False): - mapped_data["residence_city"] = location.raw.get("address", {}).get( + mapped_data["residence_city"] = location.get("address", {}).get( "city", False ) if not mapped_data.get("residence_street", False): - mapped_data["residence_street"] = location.raw.get( - "address", {} - ).get("road", False) + mapped_data["residence_street"] = location.get("address", {}).get( + "road", False + ) return mapped_data From 5cd6d303b89ff6a5940ae4362525dd40b6c15a2f Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 15 May 2024 10:34:11 +0100 Subject: [PATCH 532/547] [FIX] pms-api-rest-ocr: fix issue ocr --- pms_ocr_klippa/models/pms_property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index cf80bbed40..04c2739335 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -223,7 +223,7 @@ def _complete_residence_address(self, value, mapped_data): value["province"], self.env["res.country.state"].search(domain).mapped("name"), ) - if candidates[1] >= 90: + if candidates and candidates[1] >= 90: country_state = self.env["res.country.state"].search( domain + [("name", "=", candidates[0])] ) From a1e81289e3663f7daf75a1c39a2629a0385d0baa Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Wed, 15 May 2024 19:05:36 +0100 Subject: [PATCH 533/547] [FIX] pms-api-rest-ocr: fix naming and method return --- pms_ocr_klippa/models/pms_property.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index 04c2739335..739ed979ce 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -185,6 +185,8 @@ def _get_document_type(self, klippa_type, country_id): return document_type_id[0] 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) @@ -227,11 +229,11 @@ def _complete_residence_address(self, value, mapped_data): country_state = self.env["res.country.state"].search( domain + [("name", "=", candidates[0])] ) - mapped_data["residence_state_id"] = country_state.id + 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["residence_state_id"] = False + 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 @@ -248,10 +250,10 @@ def _complete_residence_address(self, value, mapped_data): if not mapped_data.get("residence_city", False) else mapped_data["residence_city"] ) - mapped_data["residence_state_id"] = ( + mapped_data["country_state"] = ( zip_code.city_id.state_id.id - if not mapped_data.get("residence_state_id", False) - else mapped_data["residence_state_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 @@ -345,7 +347,7 @@ def _complete_residence_address(self, value, mapped_data): country_record = ( self.env["res.country"] .with_context(lang="en_US") - .search([("name", "=", country_match_name[0])]) + .search([("name", "=", country_match[0])]) ) mapped_data["country_id"] = country_record.id if not mapped_data.get("country_state", False): From 6e8480d8371f6e2cf14adc98b8ac00c696fba98f Mon Sep 17 00:00:00 2001 From: miguelpadin Date: Thu, 16 May 2024 09:45:46 +0100 Subject: [PATCH 534/547] [FIX] pms-api-rest-ocr: fix not overwriting origin input data when checkin is saved --- pms_api_rest/services/pms_reservation_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index 9eb54b1928..c045ab466d 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -660,6 +660,8 @@ def write_reservation_checkin_partner( ): 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, From 9f9d91cd9392aa9ec2b0fd49c0a53598579734fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 16 May 2024 09:33:19 +0200 Subject: [PATCH 535/547] [ADD]pms_ocr_klippa: Add log requests --- pms_ocr_klippa/__manifest__.py | 2 + pms_ocr_klippa/models/__init__.py | 1 + pms_ocr_klippa/models/klippa_log.py | 99 ++++++++++++++++ pms_ocr_klippa/models/pms_property.py | 112 ++++++++++++++----- pms_ocr_klippa/security/ir.model.access.csv | 2 + pms_ocr_klippa/static/description/index.html | 1 - pms_ocr_klippa/views/klippa_log_views.xml | 104 +++++++++++++++++ 7 files changed, 289 insertions(+), 32 deletions(-) create mode 100644 pms_ocr_klippa/models/klippa_log.py create mode 100644 pms_ocr_klippa/security/ir.model.access.csv create mode 100644 pms_ocr_klippa/views/klippa_log_views.xml diff --git a/pms_ocr_klippa/__manifest__.py b/pms_ocr_klippa/__manifest__.py index f28b501b7b..96536750de 100644 --- a/pms_ocr_klippa/__manifest__.py +++ b/pms_ocr_klippa/__manifest__.py @@ -16,6 +16,8 @@ "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/models/__init__.py b/pms_ocr_klippa/models/__init__.py index 45a7df3f43..fc951df032 100644 --- a/pms_ocr_klippa/models/__init__.py +++ b/pms_ocr_klippa/models/__init__.py @@ -1,2 +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..eeffdbf487 --- /dev/null +++ b/pms_ocr_klippa/models/klippa_log.py @@ -0,0 +1,99 @@ +from datetime import timedelta + +from odoo import fields, models + + +class KlippaLog(models.Model): + _name = "klippa.log" + + 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="Status", + help="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", + ) + + 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 index 739ed979ce..e6900bb4e1 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -20,38 +20,88 @@ class PmsProperty(models.Model): # flake8: noqa: C901 def _klippa_document_process(self, image_base_64_front, image_base_64_back=False): - 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")) + 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, - } + headers = { + "X-Auth-Key": ocr_klippa_api_key, + "Content-Type": "application/json", + } + payload = { + "document": document, + } + 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": len(image_base_64_front) + len(image_base_64_back), + "request_headers": str(headers), + } - # Call Klippa OCR API - result = requests.post( - ocr_klippa_url, - headers=headers, - json=payload, - ) - json_data = result.json() - if json_data.get("result") != "success": - raise ValidationError(_("Error calling Klippa OCR API")) - document_data = json_data["data"]["parsed"] + # Call Klippa OCR API + result = requests.post( + ocr_klippa_url, + headers=headers, + json=payload, + ) + json_data = result.json() + log_data.extend( + { + "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, + } + ) + 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) + log_data.extend( + { + "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 as e: + log_data.extend( + { + "error": str(e), + "final_status": "error", + "total_duration": ( + datetime.now() - log_data["request_datetime"] + ).seconds, + } + ) + self.env["klippa.log"].sudo().create(log_data) + _logger.error(e) + raise ValidationError(_("Error processing Klippa document")) + + def _map_klippa_data(self, document_data): mapped_data = {} found_partner = False if document_data.get("personal_number", False): @@ -225,7 +275,7 @@ def _complete_residence_address(self, value, mapped_data): value["province"], self.env["res.country.state"].search(domain).mapped("name"), ) - if candidates and candidates[1] >= 90: + if candidates[1] >= 90: country_state = self.env["res.country.state"].search( domain + [("name", "=", candidates[0])] ) 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 index aa53b2c3c1..7043ce79f7 100644 --- a/pms_ocr_klippa/static/description/index.html +++ b/pms_ocr_klippa/static/description/index.html @@ -1,4 +1,3 @@ - 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..4a65e2677f --- /dev/null +++ b/pms_ocr_klippa/views/klippa_log_views.xml @@ -0,0 +1,104 @@ + + + + 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 + + + + + From b2e3d4a663b8da05e881fa0fe504f6779b2644f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 16 May 2024 19:41:26 +0200 Subject: [PATCH 536/547] [IMP]pms_ocr_klippa: improvement log info --- pms_ocr_klippa/models/pms_property.py | 12 ++++++++---- pms_ocr_klippa/views/klippa_log_views.xml | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index e6900bb4e1..0827564132 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -42,13 +42,16 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False 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": len(image_base_64_front) + len(image_base_64_back), + "request_size": request_size, "request_headers": str(headers), } @@ -59,7 +62,7 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False json=payload, ) json_data = result.json() - log_data.extend( + log_data.update( { "klippa_response": json_data, "klippa_status": json_data.get("result", "error"), @@ -68,6 +71,7 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False "request_duration": ( datetime.now() - log_data["request_datetime"] ).seconds, + "request_id": json_data.get("request_id", False), } ) if json_data.get("result") != "success": @@ -75,7 +79,7 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False document_data = json_data["data"]["parsed"] init_mapped_datetime = datetime.now() mapped_data = self._map_klippa_data(document_data) - log_data.extend( + log_data.update( { "service_response": mapped_data, "mapped_duration": (datetime.now() - init_mapped_datetime).seconds, @@ -88,7 +92,7 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False self.env["klippa.log"].sudo().create(log_data) return mapped_data except Exception as e: - log_data.extend( + log_data.update( { "error": str(e), "final_status": "error", diff --git a/pms_ocr_klippa/views/klippa_log_views.xml b/pms_ocr_klippa/views/klippa_log_views.xml index 4a65e2677f..26e3d6035a 100644 --- a/pms_ocr_klippa/views/klippa_log_views.xml +++ b/pms_ocr_klippa/views/klippa_log_views.xml @@ -29,7 +29,7 @@ - + From ab3fd4971372389b73b397865bf2f73991b78d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 22 May 2024 19:03:12 +0200 Subject: [PATCH 537/547] [IMP]pms_ocr_klippa: improvement mapped data and partial results --- pms_ocr_klippa/models/klippa_log.py | 12 +- pms_ocr_klippa/models/pms_property.py | 296 ++++++++++++++-------- pms_ocr_klippa/views/klippa_log_views.xml | 8 + 3 files changed, 210 insertions(+), 106 deletions(-) diff --git a/pms_ocr_klippa/models/klippa_log.py b/pms_ocr_klippa/models/klippa_log.py index eeffdbf487..a3e0ddd0b3 100644 --- a/pms_ocr_klippa/models/klippa_log.py +++ b/pms_ocr_klippa/models/klippa_log.py @@ -29,8 +29,8 @@ class KlippaLog(models.Model): help="Response", ) klippa_status = fields.Char( - string="Status", - help="Status", + string="Klippa Status", + help="Klippa Status", ) request_datetime = fields.Datetime( string="Request Date", @@ -84,6 +84,14 @@ class KlippaLog(models.Model): 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. diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index 0827564132..ee59655065 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -12,6 +12,25 @@ 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": "document_type_id.id", + "document_expedition_date": "document_expedition_date", + "document_support_number": "document_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": "document_country_id", + "zip": "partner_id.zip", +} + class PmsProperty(models.Model): _inherit = "pms.property" @@ -78,7 +97,24 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False 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, @@ -107,18 +143,7 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False def _map_klippa_data(self, document_data): mapped_data = {} - found_partner = False - if document_data.get("personal_number", False): - found_partner = ( - self.env["res.partner.id_number"] - .search( - [ - ("name", "=", document_data["personal_number"]["value"]), - ], - limit=1, - ) - .partner_id - ) + 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) @@ -131,8 +156,6 @@ def _map_klippa_data(self, document_data): # Document Data -------------------------------------------------- elif key == "issuing_country" and value: mapped_data["document_country_id"] = self._get_country_id(value) - elif key == "document_number" and value: - mapped_data["document_support_number"] = value elif key == "document_type" and value: mapped_data["document_type"] = self._get_document_type( klippa_type=value, @@ -141,9 +164,11 @@ def _map_klippa_data(self, document_data): if document_data.get("issuing_country") else False ), - ) + ).id elif key == "personal_number" and value: - mapped_data["document_number"] = 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" @@ -190,6 +215,21 @@ def _map_klippa_data(self, document_data): ).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( @@ -220,23 +260,54 @@ def _calc_expedition_date( result = date_of_expiry - relativedelta(years=10) return result if result else False - def _get_document_type(self, klippa_type, country_id): - document_type_id = False - document_type_ids = self.env["res.partner.id_category"].search( - [ - ("klippa_code", "=", klippa_type), - ] - ) - if not document_type_ids: - raise ValidationError(_(f"Document type not found: {klippa_type}")) + 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) - if len(document_type_ids) > 1: - document_type_id = document_type_ids.filtered( - lambda r: country_id in r.country_ids.ids + def _get_document_type(self, klippa_type, country_id): + # 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), + ], + limit=1, ) - if not document_type_id: - document_type_id = document_type_ids[0] - return document_type_id[0] + elif not document_type: + document_type = self.env["res.partner.id_category"].search( + [ + ("klippa_code", "=", klippa_type), + ], + limit=1, + ) + 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: @@ -346,86 +417,103 @@ def _complete_residence_address(self, value, mapped_data): ) 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 as e: + _logger.error(e) + mapped_data["nominatim_status"] = "error" + mapped_data["nominatim_response"] = str(e) + return mapped_data + + def _get_nominatim_address(self, params, street_name, mapped_data): + if street_name: + # Clean street name with mains words + street_words = street_name.split(" ") + params["street"] = " ".join( + [word for word in street_words if len(word) > 2] + ) + location = requests.get(NOMINATIM_URL, params=params) + if not location.json() or location.status_code != 200: + # If not found address, pop the street to try again if street_name: - # Clean street name with mains words - street_words = street_name.split(" ") - params["street"] = " ".join( - [word for word in street_words if len(word) > 2] - ) - location = requests.get(NOMINATIM_URL, params=params) - if not location.json() or location.status_code != 200: - # If not found address, pop the street to try again - if street_name: - params.pop("street") - location = requests.get(NOMINATIM_URL, params=params) - if location.json() and location.status_code == 200: - location = location.json()[0] - _logger.info(location) - if not mapped_data.get("zip", False): - mapped_data["zip"] = location.get("address", {}).get( - "postcode", False + params.pop("street") + location = requests.get(NOMINATIM_URL, params=params) + if location.json() and location.status_code == 200: + mapped_data["nominatim_response"] = location.json() + mapped_data["nominatim_status"] = "success" + location = location.json()[0] + _logger.info(location) + 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 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_record = self.env["res.country"].search( + [ + ( + "code", + "=", + location.get("address", {}) + .get("country_code", False) + .upper(), ) - 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_record = self.env["res.country"].search( - [ - ( - "code", - "=", - location.get("address", {}) - .get("country_code", False) - .upper(), - ) - ] + ] + ) + 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 not country_record and location.get("address", {}).get( - "country", False - ): - country_match = process.extractOne( - location.get("address", {}).get("country", False), + if country_match[1] >= 90: + country_record = ( self.env["res.country"] .with_context(lang="en_US") - .search([]) - .mapped("name"), + .search([("name", "=", country_match[0])]) ) - if country_match[1] >= 90: - country_record = ( - self.env["res.country"] - .with_context(lang="en_US") - .search([("name", "=", country_match[0])]) - ) - mapped_data["country_id"] = country_record.id - if not mapped_data.get("country_state", False): - state_name = ( - location.get("address", {}).get("province") - if location.get("address", {}).get("province") - else location.get("address", {}).get("state") + mapped_data["country_id"] = country_record.id + if not mapped_data.get("country_state", False): + state_name = ( + location.get("address", {}).get("province") + if location.get("address", {}).get("province") + else location.get("address", {}).get("state") + ) + if state_name: + country_state_record = process.extractOne( + state_name, + self.env["res.country.state"].search([]).mapped("name"), ) - if state_name: - country_state_record = process.extractOne( - state_name, - self.env["res.country.state"].search([]).mapped("name"), + if country_state_record[1] >= 90: + country_state = self.env["res.country.state"].search( + [("name", "=", country_state_record[0])] ) - if country_state_record[1] >= 90: - country_state = self.env["res.country.state"].search( - [("name", "=", country_state_record[0])] - ) - 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 - ) + 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 + ) + 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)[0]: + mapped_data[key] = document.mapped(field)[0] return mapped_data diff --git a/pms_ocr_klippa/views/klippa_log_views.xml b/pms_ocr_klippa/views/klippa_log_views.xml index 26e3d6035a..8ca869a8c5 100644 --- a/pms_ocr_klippa/views/klippa_log_views.xml +++ b/pms_ocr_klippa/views/klippa_log_views.xml @@ -27,6 +27,10 @@ + + + + @@ -34,6 +38,10 @@ + + + + From a9adebc7163b19a5c2268c34023bff3edc95485a Mon Sep 17 00:00:00 2001 From: braisab Date: Mon, 27 May 2024 10:27:05 +0200 Subject: [PATCH 538/547] [FIX]pms_api_rest: remove duplicate stateName in property datamodel --- pms_api_rest/datamodels/pms_property.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pms_api_rest/datamodels/pms_property.py b/pms_api_rest/datamodels/pms_property.py index 248a19b007..0188ea971b 100644 --- a/pms_api_rest/datamodels/pms_property.py +++ b/pms_api_rest/datamodels/pms_property.py @@ -35,7 +35,6 @@ class PmsPropertyInfo(Datamodel): street2 = fields.String(required=False, allow_none=True) zip = fields.String(required=False, allow_none=True) city = fields.String(required=False, allow_none=True) - stateName = fields.Integer(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) From d5e52d4b0bd0705aa9fa7c3c6dc6007116ca93c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 28 May 2024 17:18:40 +0200 Subject: [PATCH 539/547] [IMP]pms_ocr_klippa: improve lastname and lastname2 heuristic --- pms_ocr_klippa/models/pms_property.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index ee59655065..543e9b8ae4 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -320,10 +320,11 @@ def _get_country_id(self, country_code): def _get_surnames(self, origin_surname): # If origin surname has two or more surnames - if " " in origin_surname: - return origin_surname.split(" ") - else: - return [origin_surname, ""] + # 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): """ From 177ff1db9b2fdf668ead4f83e2e17be79480e25c Mon Sep 17 00:00:00 2001 From: braisab Date: Wed, 12 Jun 2024 12:47:35 +0200 Subject: [PATCH 540/547] [FIX]pms_api_rest: document number check has been changed from document_type aeat_identification_type to code --- pms_api_rest/services/pms_partner_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_partner_service.py b/pms_api_rest/services/pms_partner_service.py index 6413cb6095..390a26cba7 100644 --- a/pms_api_rest/services/pms_partner_service.py +++ b/pms_api_rest/services/pms_partner_service.py @@ -579,7 +579,7 @@ def check_document_number(self, document_number, document_type_id, country_id): document_type.validate_id_number(id_number) except ValidationError as e: error_mens = str(e) - if document_type.aeat_identification_type in ["02", "04"]: + if document_type.code == 'D': Partner = self.env["res.partner"] error = not Partner.simple_vat_check( country_code=country.code, From 2334743147a11e3cbcc8db70a30bdf0b15d62278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 16 Jun 2024 09:06:14 +0200 Subject: [PATCH 541/547] [FIX]pms_api_rest: send mail confirmed folio with changed body manual modifications --- pms_api_rest/services/pms_folio_service.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index cdf42d3d27..dd4aae87dc 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -1127,7 +1127,12 @@ def send_folio_mail(self, folio_id, pms_mail_info): "auto_delete": False, } if pms_mail_info.bodyMail: - email_values.update({"body": 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 From ad4418de297a6a6fa476fd95b10c01973b2ef232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 16 Jun 2024 09:07:15 +0200 Subject: [PATCH 542/547] [IMP]pms_api_rest: autofix past dates to send api client info --- pms_api_rest/models/pms_property.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pms_api_rest/models/pms_property.py b/pms_api_rest/models/pms_property.py index 37a5858390..a8034bf08d 100644 --- a/pms_api_rest/models/pms_property.py +++ b/pms_api_rest/models/pms_property.py @@ -586,11 +586,11 @@ def 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(): - raise ValidationError(_("Invalid date from")) + 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: - raise ValidationError(_("Invalid date to")) + date_to = date_from for client in clients: if not pms_property_codes: pms_properties = client.pms_property_ids From 0d008ad0dd52043225708fdeb8f3ba675dccd525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 16 Jun 2024 09:10:04 +0200 Subject: [PATCH 543/547] [IMP]pms_ocr_klippa: capture errors to system log --- pms_ocr_klippa/models/pms_property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index 543e9b8ae4..e904413e32 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -139,7 +139,7 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False ) self.env["klippa.log"].sudo().create(log_data) _logger.error(e) - raise ValidationError(_("Error processing Klippa document")) + return {} def _map_klippa_data(self, document_data): mapped_data = {} From 296111803183e13a7825e4bd0cb24d2476adbdca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 24 Jun 2024 08:39:39 +0200 Subject: [PATCH 544/547] [ADD]pms_api_rest: take account commission type in OTAs reservation price --- pms_api_rest/services/pms_folio_service.py | 27 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index dd4aae87dc..eaf7c0ade3 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -594,6 +594,9 @@ def create_folio(self, pms_folio_info): pms_folio_info.reservations, key=lambda x: x.checkout ).checkout try: + 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, @@ -606,9 +609,7 @@ def create_folio(self, pms_folio_info): else: vals = { "pms_property_id": pms_folio_info.pmsPropertyId, - "agency_id": pms_folio_info.agencyId - if pms_folio_info.agencyId - else False, + "agency_id": agency.id, "sale_channel_origin_id": self.get_channel_origin_id( pms_folio_info.saleChannelId, pms_folio_info.agencyId ), @@ -645,6 +646,9 @@ def create_folio(self, pms_folio_info): ) 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 == "substract": + commision_percent_to_deduct = agency.commission vals = { "folio_id": folio.id, "room_type_id": reservation.roomTypeId, @@ -694,13 +698,16 @@ def create_folio(self, pms_folio_info): * 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": reservationLine.price - board_day_price, + "price": price - board_day_price, "discount": reservationLine.discount, }, ) @@ -1971,6 +1978,10 @@ def wrapper_reservations(self, folio, info_reservations): external_app = self.env.user.pms_api_client cmds = [] saved_reservations = folio.reservation_ids + agency = self.env["res.partner"].browse(folio.agency_id) + commision_percent_to_deduct = 0 + if external_app and agency and agency.commission_type == "subtract": + commision_percent_to_deduct = agency.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, @@ -2085,6 +2096,7 @@ def wrapper_reservations(self, folio, info_reservations): 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}) @@ -2106,7 +2118,11 @@ def wrapper_reservations(self, folio, info_reservations): return cmds def wrapper_reservation_lines( - self, reservation, board_day_price=0, proposed_reservation=False + self, + reservation, + board_day_price=0, + proposed_reservation=False, + commission_percent_to_deduct=0, ): cmds = [] for line in reservation.reservationLines: @@ -2115,6 +2131,7 @@ def wrapper_reservation_lines( 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( From 929ca7547d9047ed464c7dd766e77ae1f832bd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Thu, 27 Jun 2024 09:24:06 +0200 Subject: [PATCH 545/547] [IMP]pms_api_rest: POST and PUT folio partnerRequests --- pms_api_rest/services/pms_folio_service.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index eaf7c0ade3..4e3627998a 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -647,7 +647,7 @@ def create_folio(self, pms_folio_info): 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 == "substract": + if external_app and agency and agency.commission_type == "subtract": commision_percent_to_deduct = agency.commission vals = { "folio_id": folio.id, @@ -666,6 +666,7 @@ def create_folio(self, pms_folio_info): "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 = [] @@ -2048,6 +2049,13 @@ def wrapper_reservations(self, folio, info_reservations): != 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}) From 75605a4ef6d1c155929f4abccc03756536cc977c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 28 Jun 2024 09:47:32 +0200 Subject: [PATCH 546/547] [FIX]pms_ocr_klippa: improvement log errors and fix search partner --- pms_ocr_klippa/models/klippa_log.py | 1 + pms_ocr_klippa/models/pms_property.py | 24 +++++++++++++++++------ pms_ocr_klippa/views/klippa_log_views.xml | 6 +++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/pms_ocr_klippa/models/klippa_log.py b/pms_ocr_klippa/models/klippa_log.py index a3e0ddd0b3..d379ddf5b6 100644 --- a/pms_ocr_klippa/models/klippa_log.py +++ b/pms_ocr_klippa/models/klippa_log.py @@ -5,6 +5,7 @@ class KlippaLog(models.Model): _name = "klippa.log" + _order = "id desc" pms_property_id = fields.Many2one( string="PMS Property", diff --git a/pms_ocr_klippa/models/pms_property.py b/pms_ocr_klippa/models/pms_property.py index e904413e32..dee2588a85 100644 --- a/pms_ocr_klippa/models/pms_property.py +++ b/pms_ocr_klippa/models/pms_property.py @@ -1,4 +1,5 @@ import logging +import traceback from datetime import date, datetime import requests @@ -130,7 +131,7 @@ def _klippa_document_process(self, image_base_64_front, image_base_64_back=False except Exception as e: log_data.update( { - "error": str(e), + "error": traceback.format_exc(), "final_status": "error", "total_duration": ( datetime.now() - log_data["request_datetime"] @@ -296,14 +297,21 @@ def _get_document_type(self, klippa_type, country_id): ("klippa_code", "=", klippa_type), ("country_ids", "=", False), ], - limit=1, ) elif not document_type: document_type = self.env["res.partner.id_category"].search( [ ("klippa_code", "=", klippa_type), ], - limit=1, + ) + if len(document_type) > 1: + # 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 + == document_data.get("document_subtype").get("value") + ) + 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") @@ -353,7 +361,7 @@ def _complete_residence_address(self, value, mapped_data): ) if candidates[1] >= 90: country_state = self.env["res.country.state"].search( - domain + [("name", "=", candidates[0])] + domain + [("name", "=", candidates[0])], limit=1 ) mapped_data["country_state"] = country_state.id if not country_record and country_state: @@ -500,7 +508,7 @@ def _get_nominatim_address(self, params, street_name, mapped_data): ) if country_state_record[1] >= 90: country_state = self.env["res.country.state"].search( - [("name", "=", country_state_record[0])] + [("name", "=", country_state_record[0])], limit=1 ) mapped_data["country_state"] = country_state.id if not mapped_data.get("residence_city", False): @@ -515,6 +523,10 @@ def _get_nominatim_address(self, params, street_name, 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)[0]: + 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/views/klippa_log_views.xml b/pms_ocr_klippa/views/klippa_log_views.xml index 8ca869a8c5..20a307f0a6 100644 --- a/pms_ocr_klippa/views/klippa_log_views.xml +++ b/pms_ocr_klippa/views/klippa_log_views.xml @@ -4,7 +4,11 @@ klippa.log.tree klippa.log - + From 60724d2e61726164fdfb7352f131ca6d85cf4b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Tue, 2 Jul 2024 19:04:05 +0200 Subject: [PATCH 547/547] [FIX]pms_api_rest: Fix create folio without agency --- pms_api_rest/services/pms_folio_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 4e3627998a..0935d3a58a 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -609,7 +609,7 @@ def create_folio(self, pms_folio_info): else: vals = { "pms_property_id": pms_folio_info.pmsPropertyId, - "agency_id": agency.id, + "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 ),