diff --git a/.copier-answers.yml b/.copier-answers.yml index 0207aeef4..762dd609b 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,13 +1,13 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: v1.1.4 +_commit: v1.3.4 _src_path: https://github.com/OCA/oca-addons-repo-template.git +ci: Travis dependency_installation_mode: OCA generate_requirements_txt: true include_wkhtmltopdf: false odoo_version: 14.0 rebel_module_groups: [] -repo_description: - All-in-One Property Management System (PMS) focused on medium-sizeations. +repo_description: All-in-One Property Management System (PMS) focused on medium-sizeations. repo_name: Property Management System repo_slug: pms travis_apt_packages: [] diff --git a/.eslintrc.yml b/.eslintrc.yml index 88f2881b4..16a185f1b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,5 +1,6 @@ env: browser: true + es6: true # See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449 parserOptions: @@ -14,7 +15,7 @@ globals: moment: readonly odoo: readonly openerp: readonly - Promise: readonly + owl: readonly # Styling is handled by Prettier, so we only need to enable AST rules; # see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 335381cb7..282a1fa70 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -10,4 +10,8 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 + with: + # The pylint-odoo version we use here does not support python 3.10 + # https://github.com/OCA/oca-addons-repo-template/issues/80 + python-version: "3.9" - uses: pre-commit/action@v2.0.0 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..1693a1253 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,69 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "0 12 * * 0" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - name: Stale PRs and issues policy + uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # General settings. + ascending: true + remove-stale-when-updated: true + # Pull Requests settings. + # 120+30 day stale policy for PRs + # * Except PRs marked as "no stale" + days-before-pr-stale: 120 + days-before-pr-close: 30 + exempt-pr-labels: "no stale" + stale-pr-label: "stale" + stale-pr-message: > + There hasn't been any activity on this pull request in the past 4 months, so + it has been marked as stale and it will be closed automatically if no + further activity occurs in the next 30 days. + + If you want this PR to never become stale, please ask a PSC member to apply + the "no stale" label. + # Issues settings. + # 180+30 day stale policy for open issues + # * Except Issues marked as "no stale" + days-before-issue-stale: 180 + days-before-issue-close: 30 + exempt-issue-labels: "no stale,needs more information" + stale-issue-label: "stale" + stale-issue-message: > + There hasn't been any activity on this issue in the past 6 months, so it has + been marked as stale and it will be closed automatically if no further + activity occurs in the next 30 days. + + If you want this issue to never become stale, please ask a PSC member to + apply the "no stale" label. + + # 15+30 day stale policy for issues pending more information + # * Issues that are pending more information + # * Except Issues marked as "no stale" + - name: Needs more information stale issues policy + uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + ascending: true + only-labels: "needs more information" + exempt-issue-labels: "no stale" + days-before-stale: 15 + days-before-close: 30 + days-before-pr-stale: -1 + days-before-pr-close: -1 + remove-stale-when-updated: true + stale-issue-label: "stale" + stale-issue-message: > + This issue needs more information and there hasn't been any activity + recently, so it has been marked as stale and it will be closed automatically + if no further activity occurs in the next 30 days. + + If you think this is a mistake, please ask a PSC member to remove the "needs + more information" label. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d7a9056a..d4fba2f21 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ exclude: | # Files and folders generated by bots, to avoid loops ^setup/|/static/description/index\.html$| # We don't want to mess with tool-generated files - .svg$|/tests/([^/]+/)?cassettes/| + .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| # Maybe reactivate this when all README files include prettier ignore tags? ^README\.md$| # Library files can have extraneous formatting (even minimized) diff --git a/README.md b/README.md index 9a8dc8ebd..ce4a07848 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ addon | version | maintainers | summary --- | --- | --- | --- [multi_pms_properties](multi_pms_properties/) | 14.0.1.0.0 | | Multi Properties Manager [payment_acquirer_multi_pms_properties](payment_acquirer_multi_pms_properties/) | 14.0.1.0.1 | | Payment Acquirer Multiproperty -[pms](pms/) | 14.0.2.20.0 | | A property management system +[pms](pms/) | 14.0.2.20.3 | | A property management system [pms_housekeeping](pms_housekeeping/) | 14.0.1.0.1 | | Housekeeping -[pms_l10n_es](pms_l10n_es/) | 14.0.2.1.1 | | PMS Spanish Adaptation +[pms_l10n_es](pms_l10n_es/) | 14.0.2.2.0 | | PMS Spanish Adaptation [pms_rooming_xls](pms_rooming_xls/) | 14.0.1.0.0 | | Rooming xlsx Management [//]: # (end addons) diff --git a/pms/__manifest__.py b/pms/__manifest__.py index d29e08382..52c4963e5 100644 --- a/pms/__manifest__.py +++ b/pms/__manifest__.py @@ -4,7 +4,7 @@ { "name": "PMS (Property Management System)", "summary": "A property management system", - "version": "14.0.2.20.0", + "version": "14.0.2.20.3", "development_status": "Alpha", "category": "Generic Modules/Property Management System", "website": "https://github.com/OCA/pms", @@ -88,6 +88,7 @@ "views/precheckin_portal_templates.xml", "wizards/wizard_massive_changes.xml", "wizards/wizard_advanced_filters.xml", + "views/payment_transaction_views.xml", ], "demo": [ "demo/pms_master_data.xml", diff --git a/pms/controllers/pms_portal.py b/pms/controllers/pms_portal.py index ef42723c4..c08a6f20a 100644 --- a/pms/controllers/pms_portal.py +++ b/pms/controllers/pms_portal.py @@ -4,6 +4,7 @@ from odoo import _, fields, http, tools from odoo.exceptions import AccessError, MissingError from odoo.http import request +from odoo.addons.payment.controllers.portal import PaymentProcessing from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager @@ -26,10 +27,127 @@ class PortalFolio(CustomerPortal): def _folio_get_page_view_values(self, folio, access_token, **kwargs): values = {"folio": folio, "token": access_token} + payment_inputs = request.env["payment.acquirer"]._get_available_payment_input( + partner=folio.partner_id, company=folio.company_id + ) + is_public_user = request.env.user._is_public() + if is_public_user: + payment_inputs.pop("pms", None) + token_count = ( + request.env["payment.token"] + .sudo() + .search_count( + [ + ("acquirer_id.company_id", "=", folio.company_id.id), + ("partner_id", "=", folio.partner_id.id), + ] + ) + ) + values["existing_token"] = token_count > 0 + values.update(payment_inputs) + values["partner_id"] = ( + folio.partner_id if is_public_user else request.env.user.partner_id, + ) return self._get_page_view_values( folio, access_token, values, "my_folios_history", False, **kwargs ) + @http.route( + "/folio/pay//form_tx", type="json", auth="public", website=True + ) + def folio_pay_form( + self, acquirer_id, folio_id, save_token=False, access_token=None, **kwargs + ): + folio_sudo = request.env["pms.folio"].sudo().browse(folio_id) + if not folio_sudo: + return False + + try: + acquirer_id = int(acquirer_id) + except Exception: + return False + + if request.env.user._is_public(): + save_token = False # we avoid to create a token for the public user + + success_url = kwargs.get( + "success_url", + "%s?%s" % (folio_sudo.access_url, access_token if access_token else ""), + ) + vals = { + "acquirer_id": acquirer_id, + "return_url": success_url, + } + + if save_token: + vals["type"] = "form_save" + transaction = folio_sudo._create_payment_transaction(vals) + PaymentProcessing.add_payment_transaction(transaction) + if not transaction: + return False + tx_ids_list = set(request.session.get("__payment_tx_ids__", [])) | set( + transaction.ids + ) + request.session["__payment_tx_ids__"] = list(tx_ids_list) + return transaction.render_folio_button( + folio_sudo, + submit_txt=_("Pay & Confirm"), + render_values={ + "type": "form_save" if save_token else "form", + "alias_usage": _( + "If we store your payment information on our server, " + "subscription payments will be made automatically." + ), + }, + ) + + # @http.route( + # '/invoice/pay//s2s_token_tx', + # type='http', + # auth='public', + # website=True + # ) + # def invoice_pay_token(self, invoice_id, pm_id=None, **kwargs): + # """ Use a token to perform a s2s transaction """ + # error_url = kwargs.get('error_url', '/my') + # access_token = kwargs.get('access_token') + # params = {} + # if access_token: + # params['access_token'] = access_token + # + # invoice_sudo = request.env['account.move'].sudo().browse(invoice_id).exists() + # if not invoice_sudo: + # params['error'] = 'pay_invoice_invalid_doc' + # return request.redirect(_build_url_w_params(error_url, params)) + # + # success_url = kwargs.get( + # 'success_url', + # "%s?%s" % ( + # invoice_sudo.access_url, + # url_encode({'access_token': access_token}) if access_token else '') + # ) + # try: + # token = request.env['payment.token'].sudo().browse(int(pm_id)) + # except (ValueError, TypeError): + # token = False + # token_owner = invoice_sudo.partner_id if \ + # request.env.user._is_public() else request.env.user.partner_id + # if not token or token.partner_id != token_owner: + # params['error'] = 'pay_invoice_invalid_token' + # return request.redirect(_build_url_w_params(error_url, params)) + # + # vals = { + # 'payment_token_id': token.id, + # 'type': 'server2server', + # 'return_url': _build_url_w_params(success_url, params), + # } + # + # tx = invoice_sudo._create_payment_transaction(vals) + # PaymentProcessing.add_payment_transaction(tx) + # + # params['success'] = 'pay_invoice' + # return request.redirect('/payment/process') + @http.route( ["/my/folios", "/my/folios/page/"], type="http", diff --git a/pms/i18n/pms.pot b/pms/i18n/pms.pot index 435b57d11..8e4e46c3e 100644 --- a/pms/i18n/pms.pot +++ b/pms/i18n/pms.pot @@ -169,6 +169,21 @@ msgstr "" msgid " Download" msgstr "" +#. module: pms +#: model_terms:ir.ui.view,arch_db:pms.portal_folio_page_payment +msgid " Pay Now" +msgstr "" + +#. module: pms +#: model_terms:ir.ui.view,arch_db:pms.portal_folio_page_payment +msgid " Paid" +msgstr "" + +#. module: pms +#: model_terms:ir.ui.view,arch_db:pms.portal_folio_page_payment +msgid " Pending" +msgstr "" + #. module: pms #: model_terms:ir.ui.view,arch_db:pms.pms_reservation_view_form msgid "" @@ -784,6 +799,18 @@ msgid "" "Customer Invoice/Credit Note" msgstr "" +#. module: pms +#: code:addons/pms/models/pms_folio.py:0 +#, python-format +msgid "A journal must be specified for the acquirer %s." +msgstr "" + +#. module: pms +#: code:addons/pms/models/pms_folio.py:0 +#, python-format +msgid "A payment acquirer is required to create a transaction." +msgstr "" + #. module: pms #: model:ir.model.fields,help:pms.field_folio_advance_payment_inv__advance_payment_method msgid "" @@ -800,6 +827,18 @@ msgid "" "A service is a non-material product you provide." msgstr "" +#. module: pms +#: code:addons/pms/models/pms_folio.py:0 +#, python-format +msgid "A transaction can't be linked to folios having different currencies." +msgstr "" + +#. module: pms +#: code:addons/pms/models/pms_folio.py:0 +#, python-format +msgid "A transaction can't be linked to folios having different partners." +msgstr "" + #. module: pms #: model:ir.model.fields,field_description:pms.field_pms_property__user_ids msgid "Accepted Users" @@ -1267,13 +1306,18 @@ msgid "Apply Pricelist" msgstr "" #. module: pms -#: model:ir.model.fields,field_description:pms.field_pms_board_service_room_type__by_default -msgid "Apply by Default" +#: model_terms:ir.ui.view,arch_db:pms.massive_changes_wizard +msgid "Apply and close" msgstr "" #. module: pms #: model_terms:ir.ui.view,arch_db:pms.massive_changes_wizard -msgid "Apply changes" +msgid "Apply and continue" +msgstr "" + +#. module: pms +#: model:ir.model.fields,field_description:pms.field_pms_board_service_room_type__by_default +msgid "Apply by Default" msgstr "" #. module: pms @@ -2200,6 +2244,11 @@ msgstr "" msgid "Class to which the room type belongs" msgstr "" +#. module: pms +#: model_terms:ir.ui.view,arch_db:pms.portal_folio_payment +msgid "Close" +msgstr "" + #. module: pms #: model:ir.model.fields,field_description:pms.field_pms_availability_plan_rule__closed #: model:ir.model.fields,field_description:pms.field_pms_massive_changes_wizard__closed @@ -2469,13 +2518,6 @@ msgstr "" msgid "Contacts" msgstr "" -#. module: pms -#: model:ir.model.fields,help:pms.field_folio_sale_line__product_uom_category_id -msgid "" -"Conversion between Units of Measure can only occur if they belong to the " -"same category. The conversion will be made based on the ratios." -msgstr "" - #. module: pms #: model:ir.model.fields,field_description:pms.field_pms_room_type__standard_price msgid "Cost" @@ -2988,7 +3030,6 @@ msgstr "" #. module: pms #: model:ir.model.fields,help:pms.field_pms_folio__credit_card_details -#: model:ir.model.fields,help:pms.field_pms_reservation__credit_card_details msgid "Details of partner credit card" msgstr "" @@ -3104,6 +3145,7 @@ msgstr "" #: model:ir.model.fields,field_description:pms.field_ir_config_parameter__display_name #: model:ir.model.fields,field_description:pms.field_ir_http__display_name #: model:ir.model.fields,field_description:pms.field_mail_compose_message__display_name +#: model:ir.model.fields,field_description:pms.field_payment_transaction__display_name #: model:ir.model.fields,field_description:pms.field_pms_advanced_filters_wizard__display_name #: model:ir.model.fields,field_description:pms.field_pms_amenity__display_name #: model:ir.model.fields,field_description:pms.field_pms_amenity_type__display_name @@ -3734,6 +3776,7 @@ msgstr "" #: model:ir.model.fields,field_description:pms.field_account_move__folio_ids #: model:ir.model.fields,field_description:pms.field_account_move_line__folio_ids #: model:ir.model.fields,field_description:pms.field_account_payment__folio_ids +#: model:ir.model.fields,field_description:pms.field_payment_transaction__folio_ids #: model:ir.model.fields,field_description:pms.field_pms_property__pms_folio_ids #: model:ir.model.fields,field_description:pms.field_res_partner__pms_folio_ids #: model:ir.model.fields,field_description:pms.field_res_users__pms_folio_ids @@ -4071,6 +4114,7 @@ msgstr "" #: model:ir.model.fields,field_description:pms.field_ir_config_parameter__id #: model:ir.model.fields,field_description:pms.field_ir_http__id #: model:ir.model.fields,field_description:pms.field_mail_compose_message__id +#: model:ir.model.fields,field_description:pms.field_payment_transaction__id #: model:ir.model.fields,field_description:pms.field_pms_advanced_filters_wizard__id #: model:ir.model.fields,field_description:pms.field_pms_amenity__id #: model:ir.model.fields,field_description:pms.field_pms_amenity_type__id @@ -4227,6 +4271,14 @@ msgstr "" msgid "If unchecked, it will allow you to hide the room type" msgstr "" +#. module: pms +#: code:addons/pms/controllers/pms_portal.py:0 +#, python-format +msgid "" +"If we store your payment information on our server, subscription payments " +"will be made automatically." +msgstr "" + #. module: pms #: model:ir.model.fields,field_description:pms.field_pms_checkin_partner__image_128 #: model:ir.model.fields,field_description:pms.field_pms_property__image_1920 @@ -4591,6 +4643,18 @@ msgstr "" msgid "Invalid reservation" msgstr "" +#. module: pms +#: code:addons/pms/models/pms_folio.py:0 +#, python-format +msgid "Invalid token found! Token acquirer %s != %s" +msgstr "" + +#. module: pms +#: code:addons/pms/models/pms_folio.py:0 +#, python-format +msgid "Invalid token found! Token partner %s != %s" +msgstr "" + #. module: pms #: model_terms:ir.ui.view,arch_db:pms.portal_my_folio_precheckin msgid "Invitation email sent" @@ -4942,6 +5006,7 @@ msgstr "" #: model:ir.model.fields,field_description:pms.field_ir_config_parameter____last_update #: model:ir.model.fields,field_description:pms.field_ir_http____last_update #: model:ir.model.fields,field_description:pms.field_mail_compose_message____last_update +#: model:ir.model.fields,field_description:pms.field_payment_transaction____last_update #: model:ir.model.fields,field_description:pms.field_pms_advanced_filters_wizard____last_update #: model:ir.model.fields,field_description:pms.field_pms_amenity____last_update #: model:ir.model.fields,field_description:pms.field_pms_amenity_type____last_update @@ -5238,6 +5303,12 @@ msgstr "" msgid "Massive changes on Pricelist & Availability Plans" msgstr "" +#. module: pms +#: code:addons/pms/wizards/wizard_massive_changes.py:0 +#, python-format +msgid "Massive changes on Pricelist and Availability Plans" +msgstr "" + #. module: pms #: model:ir.model.fields,field_description:pms.field_pms_folio__max_reservation_priority #: model:ir.model.fields,help:pms.field_pms_folio__max_reservation_priority @@ -6183,6 +6254,24 @@ msgstr "" msgid "Pay" msgstr "" +#. module: pms +#: code:addons/pms/controllers/pms_portal.py:0 +#, python-format +msgid "Pay & Confirm" +msgstr "" + +#. module: pms +#: code:addons/pms/models/payment_transaction.py:0 +#: model_terms:ir.ui.view,arch_db:pms.portal_folio_payment +#, python-format +msgid "Pay Now" +msgstr "" + +#. module: pms +#: model_terms:ir.ui.view,arch_db:pms.portal_folio_payment +msgid "Pay with" +msgstr "" + #. module: pms #: model:ir.model.fields,field_description:pms.field_pms_property__debit_limit msgid "Payable Limit" @@ -6241,6 +6330,11 @@ msgstr "" msgid "Payment Tokens" msgstr "" +#. module: pms +#: model:ir.model,name:pms.model_payment_transaction +msgid "Payment Transaction" +msgstr "" + #. module: pms #: model:ir.model.fields,help:pms.field_pms_folio__payment_term_id msgid "Payment terms for current folio." @@ -8198,6 +8292,14 @@ msgstr "" msgid "Services on Board Service included in Room" msgstr "" +#. module: pms +#: model:ir.model.fields,help:pms.field_pms_reservation__overnight_room +#: model:ir.model.fields,help:pms.field_pms_reservation_line__overnight_room +#: model:ir.model.fields,help:pms.field_pms_room_type__overnight_room +#: model:ir.model.fields,help:pms.field_pms_room_type_class__overnight +msgid "Set False if if these types of spaces are not used for overnight stays" +msgstr "" + #. module: pms #: model_terms:ir.ui.view,arch_db:pms.pms_folio_view_form msgid "Set to Done" @@ -9685,6 +9787,14 @@ msgstr "" msgid "Use a barcode to identify this contact." msgstr "" +#. module: pms +#: model:ir.model.fields,field_description:pms.field_pms_reservation__overnight_room +#: model:ir.model.fields,field_description:pms.field_pms_reservation_line__overnight_room +#: model:ir.model.fields,field_description:pms.field_pms_room_type__overnight_room +#: model:ir.model.fields,field_description:pms.field_pms_room_type_class__overnight +msgid "Use for overnight stays" +msgstr "" + #. module: pms #: model:ir.model.fields,help:pms.field_account_journal__allowed_pms_payments msgid "Use to pay for reservations" diff --git a/pms/models/__init__.py b/pms/models/__init__.py index 8b246ec75..5ab14a432 100644 --- a/pms/models/__init__.py +++ b/pms/models/__init__.py @@ -47,3 +47,4 @@ from . import account_journal from . import pms_availability from . import res_partner_id_number from . import pms_automated_mails +from . import payment_transaction diff --git a/pms/models/payment_transaction.py b/pms/models/payment_transaction.py index ca61bc106..c51a5cc21 100644 --- a/pms/models/payment_transaction.py +++ b/pms/models/payment_transaction.py @@ -1,15 +1,15 @@ -from odoo import fields, models +from odoo import _, fields, models class PaymentTransaction(models.Model): - _name = "payment.transaction" + _inherit = "payment.transaction" folio_ids = fields.Many2many( string="Folios", comodel_name="pms.folio", ondelete="cascade", - relation="account_bank_statement_folio_rel", - column1="account_journal_id", + relation="payment_transaction_folio_rel", + column1="payment_transaction_id", column2="folio_id", ) @@ -20,3 +20,23 @@ class PaymentTransaction(models.Model): if self.folio_ids: add_payment_vals["folio_ids"] = [(6, 0, self.folio_ids.ids)] return super(PaymentTransaction, self)._create_payment(add_payment_vals) + + def render_folio_button(self, folio, submit_txt=None, render_values=None): + values = { + "partner_id": folio.partner_id.id, + "type": self.type, + } + if render_values: + values.update(render_values) + return ( + self.acquirer_id.with_context( + submit_class="btn btn-primary", submit_txt=submit_txt or _("Pay Now") + ) + .sudo() + .render( + self.reference, + folio.pending_amount, + folio.currency_id.id, + values=values, + ) + ) diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index a44a21b1b..074c31625 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -325,10 +325,12 @@ class PmsCheckinPartner(models.Model): record.state = "draft" if record.reservation_id.state == "cancel": record.state = "cancel" - elif record.state in ("draft", "cancel"): + elif record.state in ("draft", "precheckin", "cancel"): if any( not getattr(record, field) - for field in record._checkin_mandatory_fields() + for field in record._checkin_mandatory_fields( + country=record.nationality_id + ) ): record.state = "draft" else: @@ -610,14 +612,9 @@ class PmsCheckinPartner(models.Model): return res @api.model - def _checkin_mandatory_fields(self, depends=False): + def _checkin_mandatory_fields(self, country=False, depends=False): mandatory_fields = [ "name", - "birthdate_date", - "gender", - "document_number", - "document_type", - "document_expedition_date", ] # api.depends need "reservation_id.state" in the lambda function if depends: diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index cd69b3d93..68c8e769e 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -185,9 +185,9 @@ class PmsFolio(models.Model): readonly=True, copy=False, comodel_name="payment.transaction", - relation="folio_transaction_rel", + relation="payment_transaction_folio_rel", column1="folio_id", - column2="transaction_id", + column2="payment_transaction_id", ) payment_ids = fields.Many2many( string="Bank Payments", @@ -964,7 +964,9 @@ class PmsFolio(models.Model): } record.update(vals) else: - journals = record.pms_property_id._get_payment_methods() + journals = record.pms_property_id._get_payment_methods( + automatic_included=True + ) paid_out = 0 for journal in journals: paid_out += sum( @@ -1955,3 +1957,78 @@ class PmsFolio(models.Model): } self.env["res.partner.id_number"].create(number_values) record.partner_id = partner + + def _create_payment_transaction(self, vals): + # Ensure the currencies are the same. + currency = self[0].currency_id + if any(folio.currency_id != currency for folio in self): + raise ValidationError( + _( + "A transaction can't be linked to folios having different currencies." + ) + ) + + # Ensure the partner are the same. + partner = self[0].partner_id + if any(folio.partner_id != partner for folio in self): + raise ValidationError( + _("A transaction can't be linked to folios having different partners.") + ) + + # Try to retrieve the acquirer. However, fallback to the token's acquirer. + acquirer_id = vals.get("acquirer_id") + acquirer = None + payment_token_id = vals.get("payment_token_id") + + if payment_token_id: + payment_token = self.env["payment.token"].sudo().browse(payment_token_id) + + # Check payment_token/acquirer matching or take the acquirer from token + if acquirer_id: + acquirer = self.env["payment.acquirer"].browse(acquirer_id) + if payment_token and payment_token.acquirer_id != acquirer: + raise ValidationError( + _("Invalid token found! Token acquirer %s != %s") + % (payment_token.acquirer_id.name, acquirer.name) + ) + if payment_token and payment_token.partner_id != partner: + raise ValidationError( + _("Invalid token found! Token partner %s != %s") + % (payment_token.partner.name, partner.name) + ) + else: + acquirer = payment_token.acquirer_id + + # Check an acquirer is there. + if not acquirer_id and not acquirer: + raise ValidationError( + _("A payment acquirer is required to create a transaction.") + ) + + if not acquirer: + acquirer = self.env["payment.acquirer"].browse(acquirer_id) + + # Check a journal is set on acquirer. + if not acquirer.journal_id: + raise ValidationError( + _("A journal must be specified for the acquirer %s.", acquirer.name) + ) + + if not acquirer_id and acquirer: + vals["acquirer_id"] = acquirer.id + + vals.update( + { + "amount": sum(self.mapped("amount_total")), + "currency_id": currency.id, + "partner_id": partner.id, + "folio_ids": [(6, 0, self.ids)], + } + ) + transaction = self.env["payment.transaction"].create(vals) + + # Process directly if payment_token + if transaction.payment_token_id: + transaction.s2s_do_transaction() + + return transaction diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index 27dcfcb56..16d9209b2 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -151,16 +151,20 @@ class PmsProperty(models.Model): @api.depends_context( "checkin", "checkout", + "real_avail", "room_type_id", "ubication_id", "capacity", "amenity_ids", "pricelist_id", + "class_id", + "overnight_rooms", "current_lines", ) def _compute_free_room_ids(self): checkin = self._context["checkin"] checkout = self._context["checkout"] + if isinstance(checkin, str): checkin = datetime.datetime.strptime( checkin, DEFAULT_SERVER_DATE_FORMAT @@ -175,11 +179,14 @@ class PmsProperty(models.Model): pricelist_id = self.env.context.get("pricelist_id", False) room_type_id = self.env.context.get("room_type_id", False) + class_id = self._context.get("class_id", False) + real_avail = self._context.get("real_avail", False) + overnight_rooms = self._context.get("overnight_rooms", False) for pms_property in self: free_rooms = pms_property.get_real_free_rooms( checkin, checkout, current_lines ) - if pricelist_id: + if pricelist_id and not real_avail: # TODO: only closed_departure take account checkout date! domain_rules = [ ("date", ">=", checkin), @@ -208,6 +215,14 @@ class PmsProperty(models.Model): free_rooms = free_rooms.filtered( lambda x: x.room_type_id.id not in room_types_to_remove ) + if class_id: + free_rooms = free_rooms.filtered( + lambda x: x.room_type_id.class_id.id == class_id + ) + if overnight_rooms: + free_rooms = free_rooms.filtered( + lambda x: x.room_type_id.overnight_room + ) if len(free_rooms) > 0: pms_property.free_room_ids = free_rooms.ids else: @@ -261,11 +276,14 @@ class PmsProperty(models.Model): @api.depends_context( "checkin", "checkout", + "real_avail", "room_type_id", "ubication_id", "capacity", "amenity_ids", "pricelist_id", + "class_id", + "overnight_rooms", "current_lines", ) def _compute_availability(self): @@ -283,12 +301,18 @@ class PmsProperty(models.Model): room_type_id = self.env.context.get("room_type_id", False) pricelist_id = self.env.context.get("pricelist_id", False) current_lines = self.env.context.get("current_lines", []) + class_id = self._context.get("class_id", False) + real_avail = self._context.get("real_avail", False) + overnight_rooms = self._context.get("overnight_rooms", False) pms_property = record.with_context( checkin=checkin, checkout=checkout, room_type_id=room_type_id, current_lines=current_lines, pricelist_id=pricelist_id, + class_id=class_id, + real_avail=real_avail, + overnight_rooms=overnight_rooms, ) count_free_rooms = len(pms_property.free_room_ids) if current_lines and not isinstance(current_lines, list): @@ -305,7 +329,7 @@ class PmsProperty(models.Model): pricelist = False if pricelist_id: pricelist = self.env["product.pricelist"].browse(pricelist_id) - if pricelist and pricelist.availability_plan_id: + if pricelist and pricelist.availability_plan_id and not real_avail: domain_rules.append( ("availability_plan_id", "=", pricelist.availability_plan_id.id) ) @@ -404,12 +428,14 @@ class PmsProperty(models.Model): dt = dt.replace(tzinfo=None) return dt - def _get_payment_methods(self): + def _get_payment_methods(self, automatic_included=False): + # We use automatic_included to True to see absolutely + # all the journals with associated payments, if it is + # false, we will only see those journals that can be used + # to pay manually self.ensure_one() payment_methods = self.env["account.journal"].search( [ - ("allowed_pms_payments", "=", True), - "&", ("type", "in", ["cash", "bank"]), "|", ("pms_property_ids", "in", self.id), @@ -422,6 +448,8 @@ class PmsProperty(models.Model): ("company_id", "=", False), ] ) + if not automatic_included: + payment_methods = payment_methods.filtered(lambda p: p.allowed_pms_payments) return payment_methods @api.model diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index afeefcf4e..b4132fcfd 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -643,7 +643,6 @@ class PmsReservation(models.Model): comodel_name="res.partner", inverse_name="reservation_possible_customer_id", ) - is_mail_send = fields.Boolean(string="Mail Sent", default=False) is_modified_reservation = fields.Boolean( @@ -652,7 +651,10 @@ class PmsReservation(models.Model): readonly=False, store=True, ) - + overnight_room = fields.Boolean( + related="room_type_id.overnight_room", + store=True, + ) lang = fields.Many2one( string="Language", comodel_name="res.lang", compute="_compute_lang" ) @@ -804,6 +806,7 @@ class PmsReservation(models.Model): "reservation_line_ids.room_id", "reservation_line_ids.occupies_availability", "preferred_room_id", + "room_type_id", "pricelist_id", "pms_property_id", ) @@ -812,7 +815,9 @@ class PmsReservation(models.Model): if reservation.checkin and reservation.checkout: if reservation.overbooking or reservation.state in ("cancel"): reservation.allowed_room_ids = self.env["pms.room"].search( - [("active", "=", True)] + [ + ("active", "=", True), + ] ) return pms_property = reservation.pms_property_id @@ -822,9 +827,12 @@ class PmsReservation(models.Model): room_type_id=False, # Allows to choose any available room current_lines=reservation.reservation_line_ids.ids, pricelist_id=reservation.pricelist_id.id, + class_id=reservation.room_type_id.class_id.id + if reservation.room_type_id + else False, + real_avail=True, ) reservation.allowed_room_ids = pms_property.free_room_ids - else: reservation.allowed_room_ids = False @@ -1045,6 +1053,7 @@ class PmsReservation(models.Model): True if ( record.reservation_type != "out" + and record.overnight_room and record.state in ["draft", "confirm", "arrival_delayed"] and record.checkin <= fields.Date.today() ) @@ -1197,9 +1206,11 @@ class PmsReservation(models.Model): reservation.commission_amount = 0 # REVIEW: Dont run with set room_type_id -> room_id(compute)-> No set adults¿? - @api.depends("preferred_room_id", "reservation_type") + @api.depends("preferred_room_id", "reservation_type", "overnight_room") def _compute_adults(self): for reservation in self: + if not reservation.overnight_room: + reservation.adults = 0 if reservation.preferred_room_id and reservation.reservation_type != "out": if reservation.adults == 0: reservation.adults = reservation.preferred_room_id.capacity @@ -1384,7 +1395,7 @@ class PmsReservation(models.Model): def _compute_checkin_partner_count(self): for record in self: - if record.reservation_type != "out": + if record.reservation_type != "out" and record.overnight_room: record.checkin_partner_count = len(record.checkin_partner_ids) record.checkin_partner_pending_count = record.adults - len( record.checkin_partner_ids @@ -1488,6 +1499,7 @@ class PmsReservation(models.Model): return [ ("state", "in", ("draft", "confirm", "arrival_delayed")), ("checkin", "<=", today), + ("adults", ">", 0), ] def _search_allowed_checkout(self, operator, value): @@ -1505,6 +1517,7 @@ class PmsReservation(models.Model): return [ ("state", "in", ("onboard", "departure_delayed")), ("checkout", ">=", today), + ("adults", ">", 0), ] def _search_allowed_cancel(self, operator, value): @@ -2014,7 +2027,9 @@ class PmsReservation(models.Model): ("checkin", "<", fields.Date.today()), ] ) - arrival_delayed_reservations.state = "arrival_delayed" + for record in arrival_delayed_reservations: + if record.overnight_room: + record.state = "arrival_delayed" @api.model def auto_departure_delayed(self): @@ -2026,8 +2041,11 @@ class PmsReservation(models.Model): ] ) for reservation in reservations: - if reservation.checkout_datetime <= fields.Datetime.now(): - reservations.state = "departure_delayed" + if reservation.overnight_room: + if reservation.checkout_datetime <= fields.Datetime.now(): + reservations.state = "departure_delayed" + else: + reservation.state = "done" def preview_reservation(self): self.ensure_one() diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index f9152badb..eb2495402 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -112,7 +112,10 @@ class PmsReservationLine(models.Model): store=True, compute="_compute_impacts_quota", ) - + overnight_room = fields.Boolean( + related="reservation_id.overnight_room", + store=True, + ) _sql_constraints = [ ( "rule_availability", @@ -175,6 +178,8 @@ class PmsReservationLine(models.Model): free_room_select = True if reservation.preferred_room_id else False # we get the rooms available for the entire stay + # (real_avail if True if the reservation was created with + # specific room selected) pms_property = line.pms_property_id pms_property = pms_property.with_context( checkin=reservation.checkin, @@ -184,6 +189,7 @@ class PmsReservationLine(models.Model): else False, current_lines=reservation.reservation_line_ids.ids, pricelist_id=reservation.pricelist_id.id, + real_avail=free_room_select, ) rooms_available = pms_property.free_room_ids diff --git a/pms/models/pms_room_type.py b/pms/models/pms_room_type.py index 116b00caa..b97cb1fa1 100644 --- a/pms/models/pms_room_type.py +++ b/pms/models/pms_room_type.py @@ -86,6 +86,10 @@ class PmsRoomType(models.Model): "Use `-1` for managing no quota.", default=-1, ) + overnight_room = fields.Boolean( + related="class_id.overnight", + store=True, + ) def name_get(self): result = [] diff --git a/pms/models/pms_room_type_class.py b/pms/models/pms_room_type_class.py index 179315ec7..6c4a07e65 100644 --- a/pms/models/pms_room_type_class.py +++ b/pms/models/pms_room_type_class.py @@ -57,6 +57,11 @@ class PmsRoomTypeClass(models.Model): help="Room type class identification code", required=True, ) + overnight = fields.Boolean( + string="Use for overnight stays", + help="Set False if if these types of spaces are not used for overnight stays", + default=True, + ) @api.model def get_unique_by_property_code(self, pms_property_id, default_code=None): diff --git a/pms/models/pms_service.py b/pms/models/pms_service.py index bd0c8492a..d52c73f92 100644 --- a/pms/models/pms_service.py +++ b/pms/models/pms_service.py @@ -299,6 +299,12 @@ class PmsService(models.Model): day_qty = 1 if service.reservation_id and service.product_id: reservation = service.reservation_id + # REVIEW: review method dependencies, reservation_line_ids + # instead of checkin/checkout + if not reservation.checkin or not reservation.checkout: + if not service.service_line_ids: + service.service_line_ids = False + continue product = service.product_id consumed_on = product.consumed_on if product.per_day: diff --git a/pms/tests/test_pms_reservation.py b/pms/tests/test_pms_reservation.py index 35c729cee..939df2aae 100644 --- a/pms/tests/test_pms_reservation.py +++ b/pms/tests/test_pms_reservation.py @@ -29,6 +29,15 @@ class TestPmsReservations(TestPms): } ) + self.room_type_triple = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Triple Test", + "default_code": "TRP_Test", + "class_id": self.room_type_class1.id, + } + ) + # create rooms self.room1 = self.env["pms.room"].create( { @@ -59,6 +68,16 @@ class TestPmsReservations(TestPms): "extra_beds_allowed": 1, } ) + + self.room4 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "Triple 104", + "room_type_id": self.room_type_triple.id, + "capacity": 3, + "extra_beds_allowed": 1, + } + ) self.partner1 = self.env["res.partner"].create( { "firstname": "Jaime", @@ -1645,6 +1664,7 @@ class TestPmsReservations(TestPms): "checkin": fields.date.today(), "checkout": fields.date.today() + datetime.timedelta(days=3), "partner_id": self.partner1.id, + "room_type_id": self.room_type_double.id, } ) @@ -1678,6 +1698,7 @@ class TestPmsReservations(TestPms): "checkin": fields.date.today() + datetime.timedelta(days=300), "checkout": fields.date.today() + datetime.timedelta(days=305), "partner_id": self.partner1.id, + "room_type_id": self.room_type_double.id, } ) r = reservation.checkin @@ -1695,8 +1716,8 @@ class TestPmsReservations(TestPms): Check available rooms after creating a reservation. ----------- Create an availability rule, create a reservation, - and then check that the allopwed_room_ids field of the - reservation and the room_type_id.room_ids field of the + and then check that the allowed_room_ids field filtered by room + type of the reservation and the room_type_id.room_ids field of the availability rule match. """ availability_rule = self.env["pms.availability.plan.rule"].create( @@ -1718,8 +1739,10 @@ class TestPmsReservations(TestPms): } ) self.assertEqual( - reservation.allowed_room_ids, - availability_rule.room_type_id.room_ids, + reservation.allowed_room_ids.filtered( + lambda r: r.room_type_id.id == availability_rule.room_type_id.id + ).ids, + availability_rule.room_type_id.room_ids.ids, "Rooms allowed don't match", ) @@ -1750,6 +1773,7 @@ class TestPmsReservations(TestPms): "checkin": fields.date.today() + datetime.timedelta(days=150), "checkout": fields.date.today() + datetime.timedelta(days=152), "agency_id": agency.id, + "room_type_id": self.room_type_double.id, } ) @@ -1794,6 +1818,7 @@ class TestPmsReservations(TestPms): "checkin": fields.date.today() + datetime.timedelta(days=150), "checkout": fields.date.today() + datetime.timedelta(days=152), "agency_id": agency.id, + "room_type_id": self.room_type_double.id, } ) self.assertEqual( @@ -1815,6 +1840,7 @@ class TestPmsReservations(TestPms): "checkin": fields.date.today() + datetime.timedelta(days=150), "checkout": fields.date.today() + datetime.timedelta(days=152), "partner_id": self.partner1.id, + "room_type_id": self.room_type_double.id, } ) @@ -1873,6 +1899,7 @@ class TestPmsReservations(TestPms): "allowed_checkin": True, "pms_property_id": self.pms_property1.id, "adults": 3, + "room_type_id": self.room_type_triple.id, } ) self.checkin1 = self.env["pms.checkin.partner"].create( @@ -1916,6 +1943,7 @@ class TestPmsReservations(TestPms): "checkout": fields.date.today(), "pms_property_id": self.pms_property1.id, "partner_id": self.host1.id, + "room_type_id": self.room_type_double.id, } ) @@ -2294,6 +2322,7 @@ class TestPmsReservations(TestPms): "partner_id": self.host1.id, "pms_property_id": self.pms_property1.id, "adults": 3, + "room_type_id": self.room_type_triple.id, } ) self.checkin1 = self.env["pms.checkin.partner"].create( @@ -2351,6 +2380,7 @@ class TestPmsReservations(TestPms): "checkout": "2014-01-17", "partner_id": self.host1.id, "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type_triple.id, "adults": 3, } ) @@ -2404,6 +2434,7 @@ class TestPmsReservations(TestPms): "partner_id": host.id, "allowed_checkout": True, "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type_double.id, } ) @@ -2435,6 +2466,7 @@ class TestPmsReservations(TestPms): "checkout": "2014-01-17", "pms_property_id": self.pms_property1.id, "folio_id": self.folio1.id, + "room_type_id": self.room_type_double.id, } ) # ACT AND ASSERT @@ -2472,6 +2504,7 @@ class TestPmsReservations(TestPms): "checkin": fields.date.today() + datetime.timedelta(days=150), "checkout": fields.date.today() + datetime.timedelta(days=152), "agency_id": agency.id, + "room_type_id": self.room_type_double.id, } ) diff --git a/pms/views/folio_portal_templates.xml b/pms/views/folio_portal_templates.xml index 65129a4ef..e6362f9ea 100644 --- a/pms/views/folio_portal_templates.xml +++ b/pms/views/folio_portal_templates.xml @@ -73,6 +73,116 @@ + + + + +