diff --git a/.copier-answers.yml b/.copier-answers.yml index 7671798888..2cd562b036 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,8 +1,9 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: v1.21.1 +_commit: v1.29 _src_path: gh:oca/oca-addons-repo-template ci: GitHub convert_readme_fragments_to_markdown: false +enable_checklog_odoo: false generate_requirements_txt: true github_check_license: true github_ci_extra_env: {} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 38b0ba110e..afd7524ef0 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -13,13 +13,13 @@ jobs: pre-commit: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" - name: Get python version run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - - uses: actions/cache@v1 + - uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3bf18e843c..5d0f95283c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest name: Detect unreleased dependencies steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | for reqfile in requirements.txt test-requirements.txt ; do if [ -f ${reqfile} ] ; then @@ -50,7 +50,7 @@ jobs: ports: - 5432:5432 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Install addons and dependencies diff --git a/.gitignore b/.gitignore index 0090721f5d..6ec07a054b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,19 @@ var/ *.egg *.eggs +# Windows installers +*.msi + +# Debian packages +*.deb + +# Redhat packages +*.rpm + +# MacOS packages +*.dmg +*.pkg + # Installer logs pip-log.txt pip-delete-this-directory.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 657b06335a..7d3274071d 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/|^.copier-answers.yml$|^.github/| + .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/|^eslint.config.cjs|^prettier.config.cjs| # Maybe reactivate this when all README files include prettier ignore tags? ^README\.md$| # Library files can have extraneous formatting (even minimized) @@ -39,7 +39,7 @@ repos: language: fail files: '[a-zA-Z0-9_]*/i18n/en\.po$' - repo: https://github.com/oca/maintainer-tools - rev: 9a170331575a265c092ee6b24b845ec508e8ef75 + rev: d5fab7ee87fceee858a3d01048c78a548974d935 hooks: # update the NOT INSTALLABLE ADDONS section above - id: oca-update-pre-commit-excluded-addons @@ -58,6 +58,8 @@ repos: hooks: - id: oca-checks-odoo-module - id: oca-checks-po + args: + - --disable=po-pretty-format - repo: https://github.com/myint/autoflake rev: v1.6.1 hooks: @@ -73,25 +75,35 @@ repos: rev: 22.8.0 hooks: - id: black - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.7.1 + - repo: local hooks: - id: prettier name: prettier (with plugin-xml) + entry: prettier + args: + - --write + - --list-different + - --ignore-unknown + types: [text] + files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$ + language: node additional_dependencies: - "prettier@2.7.1" - "@prettier/plugin-xml@2.2.0" - args: - - --plugin=@prettier/plugin-xml - files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$ - - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.24.0 + - repo: local hooks: - id: eslint - verbose: true + name: eslint + entry: eslint args: - --color - --fix + verbose: true + types: [javascript] + language: node + additional_dependencies: + - "eslint@8.24.0" + - "eslint-plugin-jsdoc@" - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: @@ -138,7 +150,7 @@ repos: - --header - "# generated from manifests external_dependencies" - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + rev: 5.0.0 hooks: - id: flake8 name: flake8 diff --git a/connector_pms/components_custom/mapper.py b/connector_pms/components_custom/mapper.py index 64e7d1081d..d99131b727 100644 --- a/connector_pms/components_custom/mapper.py +++ b/connector_pms/components_custom/mapper.py @@ -102,6 +102,10 @@ class ChannelChildMapperImport(AbstractComponent): def get_all_items(self, mapper, items, parent, to_attr, options): mapped = [] + neobookings_user = self.env["res.users"].search( + [("login", "=", "neobookings@roomdoo.com")] + ) + neobooking_item_ids = [] for item in items: map_record = mapper.map_record(item, parent=parent) if self.skip_item(map_record): @@ -110,6 +114,108 @@ def get_all_items(self, mapper, items, parent, to_attr, options): if item_values: self._child_bind(map_record, item_values) mapped.append(item_values) + if hasattr(items, "_name"): + neobooking_item_ids.append(item.id) + neobookings_property_ids = neobookings_user.pms_property_ids.ids + + if self.backend_record.pms_property_id.id not in neobookings_property_ids: + return mapped + + neobookings_property_id = self.backend_record.pms_property_id.id + neobookings_property = self.env["pms.property"].browse(neobookings_property_id) + ota_neo_settings = neobookings_property.ota_property_settings_ids.filtered( + lambda r: "eobookings" in r.agency_id.name + ) + neobookings_pricelist_id = ota_neo_settings.main_pricelist_id.id + neobookings_availability_plan_id = ota_neo_settings.main_avail_plan_id.id + payload = False + items_to_upload = False + call_type = False + min_date = False + max_date = False + room_type_ids = False + if ( + hasattr(items, "_name") + and items._name == "channel.wubook.product.pricelist.item" + ): + call_type = "prices" + items_to_upload = ( + self.env["channel.wubook.product.pricelist.item"] + .browse(neobooking_item_ids) + .filtered( + lambda r: r.pricelist_id.id == neobookings_pricelist_id + and neobookings_property_id in r.pms_property_ids.ids + ) + ) + if items_to_upload: + min_date = min(items_to_upload.mapped("date_end_consumption")) + max_date = max(items_to_upload.mapped("date_end_consumption")) + room_type_ids = ( + self.env["pms.room.type"] + .search( + [("product_id", "in", items_to_upload.mapped("product_id").ids)] + ) + .ids + ) + payload, endpoint = neobookings_property.get_payload_prices( + prices=items_to_upload, client=neobookings_user + ) + if hasattr(items, "_name") and items._name == "channel.wubook.pms.availability": + call_type = "availability" + items_to_upload = ( + self.env["channel.wubook.pms.availability"] + .browse(neobooking_item_ids) + .filtered(lambda r: r.pms_property_id.id == neobookings_property_id) + ) + if items_to_upload: + min_date = min(items_to_upload.mapped("date")) + max_date = max(items_to_upload.mapped("date")) + room_type_ids = items_to_upload.mapped("room_type_id.id") + payload, endpoint = neobookings_property.get_payload_avail( + avails=items_to_upload, client=neobookings_user + ) + if ( + hasattr(items, "_name") + and items._name == "channel.wubook.pms.availability.plan.rule" + ): + call_type = "restrictions" + items_to_upload = ( + self.env["channel.wubook.pms.availability.plan.rule"] + .browse(neobooking_item_ids) + .filtered( + lambda r: r.availability_plan_id.id + == neobookings_availability_plan_id + and r.pms_property_id.id == neobookings_property_id + ) + ) + if items_to_upload: + min_date = min(items_to_upload.mapped("date")) + max_date = max(items_to_upload.mapped("date")) + room_type_ids = items_to_upload.mapped("room_type_id.id") + payload, endpoint = neobookings_property.get_payload_rules( + rules=items_to_upload, client=neobookings_user + ) + if payload: + _logger.info("Exporting Neobookings") + response = neobookings_property.pms_api_push_payload( + payload=payload, endpoint=endpoint, client=neobookings_user + ) + self.env["pms.api.log"].sudo().create( + { + "pms_property_id": neobookings_property_id, + "client_id": neobookings_user.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": min_date, + "target_date_to": max_date, + "request_type": call_type, + "room_type_ids": room_type_ids, + } + ) return mapped def get_items(self, items, parent, to_attr, options): diff --git a/connector_pms_wubook/models/pms_availability/binding.py b/connector_pms_wubook/models/pms_availability/binding.py index 4c59da8bba..f1125d7dfe 100644 --- a/connector_pms_wubook/models/pms_availability/binding.py +++ b/connector_pms_wubook/models/pms_availability/binding.py @@ -74,8 +74,8 @@ def _compute_sale_avail(self): else: raise ValidationError( _( - "More than one rule found, you need to" - " specify the rule in the context" + """More than one rule found, you need to + specify the rule in the context""" ) ) if record.sale_avail != sale_avail: @@ -164,7 +164,8 @@ def create(self, vals): def _write(self, vals): cr = self._cr - if any([field in vals for field in AUTO_EXPORT_FIELDS]): + export_fields = AUTO_EXPORT_FIELDS + if any([field in vals for field in export_fields]): query = 'UPDATE "%s" SET "actual_write_date"=%s WHERE id IN %%s' % ( self._table, AsIs("(now() at time zone 'UTC')"), diff --git a/multi_pms_properties/i18n/multi_pms_properties.pot b/multi_pms_properties/i18n/multi_pms_properties.pot index a11d509b21..a59199049a 100644 --- a/multi_pms_properties/i18n/multi_pms_properties.pot +++ b/multi_pms_properties/i18n/multi_pms_properties.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 14.0\n" +"Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" diff --git a/multi_pms_properties/tests/test_multi_pms_properties.py b/multi_pms_properties/tests/test_multi_pms_properties.py index 10f81e0226..8bff62a914 100644 --- a/multi_pms_properties/tests/test_multi_pms_properties.py +++ b/multi_pms_properties/tests/test_multi_pms_properties.py @@ -12,7 +12,7 @@ @common.tagged("-at_install", "post_install") -class TestMultiPMSProperties(common.SavepointCase): +class TestMultiPMSProperties(common.TransactionCase): @classmethod def setUpClass(cls): super(TestMultiPMSProperties, cls).setUpClass() diff --git a/pms/models/account_payment.py b/pms/models/account_payment.py index 380d1630a0..8106a740a8 100644 --- a/pms/models/account_payment.py +++ b/pms/models/account_payment.py @@ -119,7 +119,9 @@ def action_draft(self): } for move in downpayment_invoices ] - downpayment_invoices._reverse_moves( + downpayment_invoices.with_context( + {"sii_refund_type": "I"} + )._reverse_moves( default_values_list, cancel=True ) else: diff --git a/pms/models/pms_availability.py b/pms/models/pms_availability.py index b60b3081fc..80ed94fd2d 100644 --- a/pms/models/pms_availability.py +++ b/pms/models/pms_availability.py @@ -88,7 +88,7 @@ class PmsAvailability(models.Model): @api.depends( "reservation_line_ids", "reservation_line_ids.occupies_availability", - "room_type_id.total_rooms_count", + # "room_type_id.total_rooms_count", "parent_avail_id", "parent_avail_id.reservation_line_ids", "parent_avail_id.reservation_line_ids.occupies_availability", diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 4c2a59e156..84d389d823 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -1446,7 +1446,7 @@ def _compute_checkin_partner_count(self): record.checkin_partner_count = 0 record.checkin_partner_pending_count = 0 - @api.depends("room_type_id", "partner_id") + @api.depends("room_type_id") def _compute_tax_ids(self): for record in self: record = record.with_company(record.company_id) diff --git a/pms/models/pms_service.py b/pms/models/pms_service.py index b24ed37140..45e598467f 100644 --- a/pms/models/pms_service.py +++ b/pms/models/pms_service.py @@ -232,7 +232,7 @@ def _compute_pricelist_id(self): origin = record.reservation_id if record.reservation_id else record.folio_id record.pricelist_id = origin.pricelist_id - @api.depends("product_id", "folio_id.partner_id", "reservation_id.partner_id") + @api.depends("product_id") def _compute_tax_ids(self): for service in self: partner = ( diff --git a/pms/models/res_users.py b/pms/models/res_users.py index 8c9fcd5fd8..85f387a242 100644 --- a/pms/models/res_users.py +++ b/pms/models/res_users.py @@ -28,7 +28,9 @@ class ResUsers(models.Model): def _is_property_member(self, pms_property_id): self.ensure_one() # TODO: Use pms_teams and roles to check if user is member of property - # and analice the management of external users like a Call Center + # and analice the management of external users like a Call + if "aldahotels" in self.login: + True return self.env.user.has_group( "pms.group_pms_user" ) and not self.env.user.has_group("pms.group_pms_call") diff --git a/pms_account_move_budget/README.rst b/pms_account_move_budget/README.rst new file mode 100644 index 0000000000..80e8582e6f --- /dev/null +++ b/pms_account_move_budget/README.rst @@ -0,0 +1,71 @@ +=============================== +Property in Account Move Budget +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6815896ead9b61b022c88f9507e7e1bb2442a2f44e39e64ad71767c7db86e830 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.0/pms_account_move_budget + :alt: OCA/pms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pms-16-0/pms-16-0-pms_account_move_budget + :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=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds the pms property relational field to the account move budget model to enable the use of properties in accounting budgets. + +**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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Comunitea + +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_account_move_budget/__init__.py b/pms_account_move_budget/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pms_account_move_budget/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pms_account_move_budget/__manifest__.py b/pms_account_move_budget/__manifest__.py new file mode 100644 index 0000000000..b7e77014f1 --- /dev/null +++ b/pms_account_move_budget/__manifest__.py @@ -0,0 +1,20 @@ +# © 2023 Comunitea +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Property in Account Move Budget", + "summary": "Add Property Field in Account Move Budget", + "version": "16.0.1.0.0", + "category": "Custom", + "website": "https://github.com/OCA/pms", + "author": "Comunitea, Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "account_move_budget", + "pms", + ], + "data": [ + "views/account_move_budget_line_view.xml", + ], +} diff --git a/pms_account_move_budget/i18n/pms_account_move_budget.pot b/pms_account_move_budget/i18n/pms_account_move_budget.pot new file mode 100644 index 0000000000..f175503617 --- /dev/null +++ b/pms_account_move_budget/i18n/pms_account_move_budget.pot @@ -0,0 +1,39 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pms_account_move_budget +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: pms_account_move_budget +#: model:ir.model,name:pms_account_move_budget.model_account_move_budget_line +msgid "Account Move Budget Line" +msgstr "" + +#. module: pms_account_move_budget +#: model:ir.model.fields,field_description:pms_account_move_budget.field_account_move_budget_line__display_name +msgid "Display Name" +msgstr "" + +#. module: pms_account_move_budget +#: model:ir.model.fields,field_description:pms_account_move_budget.field_account_move_budget_line__pms_property_id +msgid "Hotel" +msgstr "" + +#. module: pms_account_move_budget +#: model:ir.model.fields,field_description:pms_account_move_budget.field_account_move_budget_line__id +msgid "ID" +msgstr "" + +#. module: pms_account_move_budget +#: model:ir.model.fields,field_description:pms_account_move_budget.field_account_move_budget_line____last_update +msgid "Last Modified on" +msgstr "" diff --git a/pms_account_move_budget/models/__init__.py b/pms_account_move_budget/models/__init__.py new file mode 100644 index 0000000000..d218e53943 --- /dev/null +++ b/pms_account_move_budget/models/__init__.py @@ -0,0 +1 @@ +from . import account_move_budget_line diff --git a/pms_account_move_budget/models/account_move_budget_line.py b/pms_account_move_budget/models/account_move_budget_line.py new file mode 100644 index 0000000000..ceecb30870 --- /dev/null +++ b/pms_account_move_budget/models/account_move_budget_line.py @@ -0,0 +1,12 @@ +# Copyright 2023 Comunitea S.L. (http://www.comunitea.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AccountMoveBudgetLine(models.Model): + _inherit = "account.move.budget.line" + + pms_property_id = fields.Many2one( + "pms.property", string="Hotel", required=False, index=True + ) diff --git a/pms_account_move_budget/readme/DESCRIPTION.rst b/pms_account_move_budget/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..ba286df64d --- /dev/null +++ b/pms_account_move_budget/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds the pms property relational field to the account move budget model to enable the use of properties in accounting budgets. diff --git a/pms_account_move_budget/static/description/icon.png b/pms_account_move_budget/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/pms_account_move_budget/static/description/icon.png differ diff --git a/pms_account_move_budget/static/description/index.html b/pms_account_move_budget/static/description/index.html new file mode 100644 index 0000000000..3ea0adea44 --- /dev/null +++ b/pms_account_move_budget/static/description/index.html @@ -0,0 +1,416 @@ + + + + + +Property in Account Move Budget + + + +
+

Property in Account Move Budget

+ + +

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

+

This module adds the pms property relational field to the account move budget model to enable the use of properties in accounting budgets.

+

Table of contents

+ +
+

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

+
    +
  • Comunitea
  • +
+
+
+

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_account_move_budget/views/account_move_budget_line_view.xml b/pms_account_move_budget/views/account_move_budget_line_view.xml new file mode 100644 index 0000000000..9d56fb4b9f --- /dev/null +++ b/pms_account_move_budget/views/account_move_budget_line_view.xml @@ -0,0 +1,19 @@ + + + + + Account Move Budget Line Hotel tree + account.move.budget.line + + + + + + + + + diff --git a/pms_api_rest/__manifest__.py b/pms_api_rest/__manifest__.py index 48bd5c6d0f..cd44ddce9d 100644 --- a/pms_api_rest/__manifest__.py +++ b/pms_api_rest/__manifest__.py @@ -16,6 +16,7 @@ "l10n_es_aeat", "sql_export_excel", "feed_rss", + "sale_loyalty", ], "external_dependencies": { "python": ["jwt", "simplejson", "marshmallow", "jose"], diff --git a/pms_api_rest/services/pms_folio_service.py b/pms_api_rest/services/pms_folio_service.py index 7966fbec97..04078b7146 100644 --- a/pms_api_rest/services/pms_folio_service.py +++ b/pms_api_rest/services/pms_folio_service.py @@ -80,7 +80,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, + createdBy=folio.create_uid.name if folio.create_uid else "Unknown", internalComment=folio.internal_comment if folio.internal_comment else None, @@ -946,6 +946,13 @@ def create_folio(self, pms_folio_info): )._compute_board_service_room_id() if reservation.stateCode == "cancel": reservation_record.action_cancel() + # Apply loyalty discount + for reservation in folio.reservation_ids: + service_discount_cmds = self.get_autoinclude_loyalty_by_code( + reservation, pms_folio_info.internalComment or "" + ) + if len(service_discount_cmds) > 0: + reservation.write({"service_ids": service_discount_cmds}) pms_folio_info.transactions = self.normalize_payments_structure( pms_folio_info, folio ) @@ -2799,3 +2806,57 @@ def get_folio_payment_link(self, folio_id, folio_payment_link_search_param): return PmsFolioPaymentLinkInfo( paymentLink=payment_link, ) + + def get_autoinclude_loyalty_by_code(self, reservation, internal_comment=False): + """ + This method is used to get the autoinclude loyalty by code + """ + promos = ( + self.env["loyalty.program"] + .sudo() + .search( + [ + ("company_id", "=", reservation.company_id.id), + ] + ) + ) + cmds = [] + if not internal_comment: + return cmds + for promo in promos.filtered( + lambda p: p.rule_ids.filtered( + lambda r: r.code and r.code in internal_comment + ) + ): + reward = promo.reward_ids[0] if promo.reward_ids else False + if not reward: + continue + product = reward.discount_line_product_id + if promo.reward_type == "percentage": + price = -reward.discount * reservation.price_room_services_set / 100 + else: + price = -reward.discount + if not product or price == 0: + continue + cmds.append( + ( + 0, + False, + { + "product_id": product.id, + "reservation_id": reservation.id, + "service_line_ids": [ + ( + 0, + False, + { + "date": reservation.checkin, + "price_unit": price, + "day_qty": 1, + }, + ) + ], + }, + ) + ) + return cmds diff --git a/pms_api_rest/services/pms_invoice_service.py b/pms_api_rest/services/pms_invoice_service.py index 6f7c57fbfb..7178483f1a 100644 --- a/pms_api_rest/services/pms_invoice_service.py +++ b/pms_api_rest/services/pms_invoice_service.py @@ -298,7 +298,12 @@ def update_invoice(self, invoice_id, pms_invoice_info): } for move in invoice ] - invoice._reverse_moves(default_values_list, cancel=True) + invoice.with_context( + { + "sii_refund_type": "I", + "supplier_invoice_number": invoice.name, + } + )._reverse_moves(default_values_list, cancel=True) new_invoice.write(new_vals) new_invoice.sudo().action_post() else: diff --git a/pms_api_rest/services/pms_reservation_service.py b/pms_api_rest/services/pms_reservation_service.py index f6e02b553a..f7ad528744 100644 --- a/pms_api_rest/services/pms_reservation_service.py +++ b/pms_api_rest/services/pms_reservation_service.py @@ -155,7 +155,9 @@ 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, + createdBy=reservation.create_uid.name + if reservation.create_uid + else "Unknown", segmentationId=reservation.segmentation_ids[0].id if reservation.segmentation_ids else None, diff --git a/pms_l10n_es/models/pms_reservation.py b/pms_l10n_es/models/pms_reservation.py index 41364fbd26..8ceff080ba 100644 --- a/pms_l10n_es/models/pms_reservation.py +++ b/pms_l10n_es/models/pms_reservation.py @@ -90,7 +90,7 @@ def _compute_ses_status_traveller_report(self): ) @api.model - def create_communication(self, reservation_id, operation, entity): + def create_communication(self, reservation_id, operation, entity, communication_id_to_cancel=False): reservation = self.env["pms.reservation"].browse(reservation_id) self.env["pms.ses.communication"].create( { @@ -98,6 +98,7 @@ def create_communication(self, reservation_id, operation, entity): "operation": operation, "entity": entity, "room_id": reservation.preferred_room_id.id, + "communication_id_to_cancel": communication_id_to_cancel, } ) @@ -152,7 +153,10 @@ def create_communication_after_update_reservation(self, reservation, vals): and last_communication.operation == CREATE_OPERATION_CODE ): self.create_communication( - reservation.id, DELETE_OPERATION_CODE, "RH" + reservation.id, + DELETE_OPERATION_CODE, + "RH", + last_communication.id, ) elif vals["state"] != "cancel" and ( last_communication.operation == DELETE_OPERATION_CODE diff --git a/pms_l10n_es/models/pms_ses_communication.py b/pms_l10n_es/models/pms_ses_communication.py index bc77aa0eb9..6e7b052088 100644 --- a/pms_l10n_es/models/pms_ses_communication.py +++ b/pms_l10n_es/models/pms_ses_communication.py @@ -101,6 +101,12 @@ class PmsSesCommunication(models.Model): help="Number of attempts to send the communication", default=0, ) + communication_id_to_cancel = fields.Many2one( + comodel_name="pms.ses.communication", + string="Communication to Cancel", + help="Communication to cancel if this is a cancellation operation", + ) + def force_send_communication(self): for record in self: diff --git a/pms_l10n_es/wizards/traveller_report.py b/pms_l10n_es/wizards/traveller_report.py index 61be4b4bf1..60482a79b3 100644 --- a/pms_l10n_es/wizards/traveller_report.py +++ b/pms_l10n_es/wizards/traveller_report.py @@ -88,7 +88,9 @@ def _ses_xml_contract_elements(comunicacion, reservation, people=False): if people: ET.SubElement(contrato, "numPersonas").text = str(people) else: - ET.SubElement(contrato, "numPersonas").text = str(reservation.adults) + ET.SubElement(contrato, "numPersonas").text = str( + reservation.adults + reservation.children if reservation.children else 0 + ) _ses_xml_payment_elements(contrato, reservation) @@ -134,7 +136,10 @@ def _ses_xml_person_names_elements(persona, reservation, checkin_partner): ] elif ( reservation.partner_name - and len(replace_multiple_spaces(reservation.partner_name).split(" ")) > 1 + and len( + replace_multiple_spaces(reservation.partner_name.rstrip()).split(" ") + ) + > 1 ): ses_lastname = clean_string_only_letters( replace_multiple_spaces(reservation.partner_name) @@ -1170,8 +1175,11 @@ def ses_send_communications(self, entity, pms_ses_communication_id=False): communication.reservation_id.pms_property_id.institution_lessor_id ) ses_url = communication.reservation_id.pms_property_id.ses_url - if communication.operation == DELETE_OPERATION_CODE: - communication_to_cancel = self.env["pms.ses.communication"].search( + if ( + communication.operation == DELETE_OPERATION_CODE + and communication.communication_id_to_cancel + ): + """communication_to_cancel = self.env["pms.ses.communication"].search( [ ("reservation_id", "=", communication.reservation_id.id), ("state", "!=", "to_send"), @@ -1180,12 +1188,12 @@ def ses_send_communications(self, entity, pms_ses_communication_id=False): ], order="id desc", limit=1, - ) + )""" data = ( "' + "" - + communication_to_cancel.communication_id + + communication.communication_id_to_cancel.communication_id + "" + "" ) diff --git a/pms_l10n_es_sii/README.rst b/pms_l10n_es_sii/README.rst new file mode 100644 index 0000000000..a91a546dd6 --- /dev/null +++ b/pms_l10n_es_sii/README.rst @@ -0,0 +1,84 @@ +======================== +PMS AEAT SII Integration +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9d838c74a50e3126ae042ae24856dec9752450df7d9807c22ff328ef03a83a4c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.0/pms_l10n_es_sii + :alt: OCA/pms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pms-16-0/pms-16-0-pms_l10n_es_sii + :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=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Set the pms various contact like anonymous SII aeat + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Nothing to do + +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 +~~~~~~~~~~~~ + +* `Commit [Sun] `: + + * Dario Lodeiros + * Mario Montes + +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..572cd51840 --- /dev/null +++ b/pms_l10n_es_sii/__init__.py @@ -0,0 +1 @@ +# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html diff --git a/pms_l10n_es_sii/__manifest__.py b/pms_l10n_es_sii/__manifest__.py new file mode 100644 index 0000000000..fada652742 --- /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": "16.0.1.1.0", + "license": "AGPL-3", + "depends": [ + "l10n_es_aeat_sii_oca", + "pms", + ], + "data": ["data/pms_data.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..35dc905460 --- /dev/null +++ b/pms_l10n_es_sii/data/pms_data.xml @@ -0,0 +1,7 @@ + + + + + True + + diff --git a/pms_l10n_es_sii/i18n/pms_l10n_es_sii.pot b/pms_l10n_es_sii/i18n/pms_l10n_es_sii.pot new file mode 100644 index 0000000000..78d58d53fe --- /dev/null +++ b/pms_l10n_es_sii/i18n/pms_l10n_es_sii.pot @@ -0,0 +1,13 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/pms_l10n_es_sii/readme/CONTRIBUTORS.rst b/pms_l10n_es_sii/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..372695dbd2 --- /dev/null +++ b/pms_l10n_es_sii/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Commit [Sun] `: + + * Dario Lodeiros + * Mario Montes diff --git a/pms_l10n_es_sii/readme/DESCRIPTION.rst b/pms_l10n_es_sii/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..6386051ecc --- /dev/null +++ b/pms_l10n_es_sii/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Set the pms various contact like anonymous SII aeat diff --git a/pms_l10n_es_sii/readme/USAGE.rst b/pms_l10n_es_sii/readme/USAGE.rst new file mode 100644 index 0000000000..b1534b1602 --- /dev/null +++ b/pms_l10n_es_sii/readme/USAGE.rst @@ -0,0 +1 @@ +Nothing to do 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 0000000000..3a0328b516 Binary files /dev/null and b/pms_l10n_es_sii/static/description/icon.png differ 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..09b9f72731 --- /dev/null +++ b/pms_l10n_es_sii/static/description/index.html @@ -0,0 +1,432 @@ + + + + + +PMS AEAT SII Integration + + + +
+

PMS AEAT SII Integration

+ + +

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

+

Set the pms various contact like anonymous SII aeat

+

Table of contents

+ +
+

Usage

+

Nothing to do

+
+
+

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

+
    +
  • Commit [Sun] <https://www.commitsun.com>: +
  • +
+
+
+

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/pos_pms_link/README.rst b/pos_pms_link/README.rst new file mode 100644 index 0000000000..15f1849213 --- /dev/null +++ b/pos_pms_link/README.rst @@ -0,0 +1,29 @@ +POS PMS LINK +======================= + +**Table of contents** + +.. contents:: + :local: + +Settings +-------- + +- PMS Service product needs to be avaible in pos (available_in_pos = True) to add it from pms.service.line. +- On pos.config you can mark pay_on_reservation = True to be able to pay with reservations. After that select a pos.payment.method that will be used after selecting the reservation. + + +Add reservation services to pos.order +------------------------------------- + +- While on a pos.order click on the 'Reservation' button. A modal will open, select the desired reservation and the lines will be added as pos.order.lines. + - Only the lines of the service with the current date will be added. + - This will only add the quantity of the lines that is not already linked to another pos.order.line. + +Pay pos.order on pms.reservation +-------------------------------- + +- if pay_on_reservation is active on pos.config, the payment screen will show the payment method: 'Reservation'. +- Click on it and the reservation modal will open, select the resired reservation, the pos.order will be validated and you will be shown the bill printing screen. + - This will add the payment method selected in the pos.config. + - pos.order.lines will be added as pms.service.lines in the reservation. diff --git a/pos_pms_link/__init__.py b/pos_pms_link/__init__.py new file mode 100644 index 0000000000..0106bf4860 --- /dev/null +++ b/pos_pms_link/__init__.py @@ -0,0 +1,21 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import models diff --git a/pos_pms_link/__manifest__.py b/pos_pms_link/__manifest__.py new file mode 100644 index 0000000000..e32f8ac5f5 --- /dev/null +++ b/pos_pms_link/__manifest__.py @@ -0,0 +1,53 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + "name": "POS PMS link", + "summary": "Allows to use PMS reservations on the POS interface", + "version": "16.0.1.0.0", + "author": "Comunitea Servicios Tecnológicos S.L., Odoo Community Association (OCA)", + "website": "https://github.com/OCA/pms", + "license": "AGPL-3", + "category": "Point of Sale", + "depends": [ + "point_of_sale", + "pms", + "pos_hr", + ], + "data": [ + # "views/assets_common.xml", + "views/pms_service_line.xml", + "views/pos_order.xml", + "views/res_config_settings.xml", + ], + "demo": [], + "assets": { + "point_of_sale.assets": [ + "pos_pms_link/static/src/js/*.js", + "pos_pms_link/static/src/js/*/*.js", + "pos_pms_link/static/src/js/*/*/*.js", + "pos_pms_link/static/src/xml/*.xml", + "pos_pms_link/static/src/xml/*/*.xml", + "pos_pms_link/static/src/xml/*/*/*.xml", + "pos_pms_link/static/src/scss/*.scss", + ], + }, + "installable": True, +} diff --git a/pos_pms_link/models/__init__.py b/pos_pms_link/models/__init__.py new file mode 100644 index 0000000000..ef1f654642 --- /dev/null +++ b/pos_pms_link/models/__init__.py @@ -0,0 +1,30 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import pos_order +from . import pms_service_line +from . import pos_config +from . import res_config_settings +from . import pos_payment +from . import pms_reservation +from . import pms_service +from . import product_pricelist +from . import pos_session +from . import res_partner diff --git a/pos_pms_link/models/pms_reservation.py b/pos_pms_link/models/pms_reservation.py new file mode 100644 index 0000000000..ac55d0bf67 --- /dev/null +++ b/pos_pms_link/models/pms_reservation.py @@ -0,0 +1,38 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from odoo import api, models + + +class PMSReservation(models.Model): + _inherit = "pms.reservation" + + @api.model + def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): + if self.env.context.get("pos_user_force", False): + return ( + super() + .sudo() + .with_context(pos_user_force=False) + .search_read(domain, fields, offset, limit, order) + ) + else: + return super(PMSReservation, self).search_read( + domain, fields, offset, limit, order + ) diff --git a/pos_pms_link/models/pms_service.py b/pos_pms_link/models/pms_service.py new file mode 100644 index 0000000000..57572263af --- /dev/null +++ b/pos_pms_link/models/pms_service.py @@ -0,0 +1,49 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from odoo import api, models + + +class PMSService(models.Model): + _inherit = "pms.service" + + @api.model + def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): + if self.env.context.get("pos_user_force", False): + return ( + super() + .sudo() + .with_context(pos_user_force=False) + .search_read(domain, fields, offset, limit, order) + ) + else: + return super(PMSService, self).search_read( + domain, fields, offset, limit, order + ) + + @api.model + def create_from_ui(self, reservation): + """create or modify a reservation from the point of sale ui. + reservation contains the reservation's fields.""" + reservation_id = reservation.pop("id", False) + if reservation_id: # Modifying existing reservation + self.browse(reservation_id).write(reservation) + else: + reservation_id = self.create(reservation).id + return reservation_id diff --git a/pos_pms_link/models/pms_service_line.py b/pos_pms_link/models/pms_service_line.py new file mode 100644 index 0000000000..a686f3e906 --- /dev/null +++ b/pos_pms_link/models/pms_service_line.py @@ -0,0 +1,44 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from odoo import api, fields, models + + +class PMSServiceLine(models.Model): + _inherit = "pms.service.line" + + pos_order_line_ids = fields.One2many( + string="POS lines", + comodel_name="pos.order.line", + inverse_name="pms_service_line_id", + ) + + @api.model + def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): + if self.env.context.get("pos_user_force", False): + return ( + super() + .sudo() + .with_context(pos_user_force=False) + .search_read(domain, fields, offset, limit, order) + ) + else: + return super(PMSServiceLine, self).search_read( + domain, fields, offset, limit, order + ) diff --git a/pos_pms_link/models/pos_config.py b/pos_pms_link/models/pos_config.py new file mode 100644 index 0000000000..410555fbb8 --- /dev/null +++ b/pos_pms_link/models/pos_config.py @@ -0,0 +1,56 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2023 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class PosConfig(models.Model): + _inherit = "pos.config" + + pay_on_reservation = fields.Boolean("Pay on reservation", default=False) + pay_on_reservation_method_id = fields.Many2one( + "pos.payment.method", string="Pay on reservation method" + ) + reservation_allowed_propertie_ids = fields.Many2many( + "pms.property", string="Reservation allowed properties" + ) + close_session_allowed = fields.Boolean("Close session allowed", default=False) + + cash_in_out_allowed = fields.Boolean("Cash in/out allowed", default=False) + + cash_move_partner = fields.Boolean("Use partner in cash moves", default=False) + + @api.model + def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): + if self.env.context.get("pos_user_force", False): + return ( + super() + .sudo() + .with_context(pos_user_force=False) + .search_read(domain, fields, offset, limit, order) + ) + else: + return super(PosConfig, self).search_read( + domain, fields, offset, limit, order + ) diff --git a/pos_pms_link/models/pos_order.py b/pos_pms_link/models/pos_order.py new file mode 100644 index 0000000000..541c9024e8 --- /dev/null +++ b/pos_pms_link/models/pos_order.py @@ -0,0 +1,115 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from datetime import datetime + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class PosOrder(models.Model): + _inherit = "pos.order" + + paid_on_reservation = fields.Boolean("Paid on reservation", default=False) + pms_reservation_id = fields.Many2one("pms.reservation", string="PMS reservation") + + def _get_fields_for_draft_order(self): + res = super(PosOrder, self)._get_fields_for_draft_order() + res.append("paid_on_reservation") + res.append("pms_reservation_id") + return res + + @api.model + def _order_fields(self, ui_order): + order_fields = super(PosOrder, self)._order_fields(ui_order) + order_fields["paid_on_reservation"] = ui_order.get("paid_on_reservation", False) + order_fields["pms_reservation_id"] = ui_order.get("pms_reservation_id", False) + return order_fields + + def _get_fields_for_order_line(self): + res = super(PosOrder, self)._get_fields_for_order_line() + res.append("pms_service_line_id") + return res + + def _get_order_lines(self, orders): + super(PosOrder, self)._get_order_lines(orders) + for order in orders: + if "lines" in order: + for line in order["lines"]: + line[2]["pms_service_line_id"] = ( + line[2]["pms_service_line_id"][0] + if line[2]["pms_service_line_id"] + else False + ) + + @api.model + def _process_order(self, pos_order, draft, existing_order): + data = pos_order.get("data", False) + if ( + data + and data.get("paid_on_reservation", False) + and data.get("pms_reservation_id", False) + ): + pms_reservation_id = data.pop("pms_reservation_id") + res = super(PosOrder, self)._process_order(pos_order, draft, existing_order) + order_id = self.env["pos.order"].browse(res) + pms_reservation_id = ( + self.sudo().env["pms.reservation"].browse(pms_reservation_id) + ) + if not pms_reservation_id: + raise UserError(_("Reservation does not exists.")) + order_id.pms_reservation_id = pms_reservation_id.id + order_id.add_order_lines_to_reservation(pms_reservation_id) + return res + else: + return super()._process_order(pos_order, draft, existing_order) + + def add_order_lines_to_reservation(self, pms_reservation_id): + self.lines.filtered(lambda x: not x.pms_service_line_id)._generate_pms_service( + pms_reservation_id + ) + + +class PosOrderLine(models.Model): + _inherit = "pos.order.line" + + pms_service_line_id = fields.Many2one("pms.service.line", string="PMS Service line") + + def _generate_pms_service(self, pms_reservation_id): + for line in self: + vals = { + "product_id": line.product_id.id, + "reservation_id": pms_reservation_id.id, + "is_board_service": False, + "service_line_ids": [ + ( + 0, + False, + { + "date": datetime.now(), + "price_unit": line.price_unit, + "discount": line.discount, + "day_qty": line.qty, + }, + ) + ], + } + service = self.sudo().env["pms.service"].create(vals) + + line.write({"pms_service_line_id": service.service_line_ids.id}) diff --git a/pos_pms_link/models/pos_payment.py b/pos_pms_link/models/pos_payment.py new file mode 100644 index 0000000000..2f9e84aca8 --- /dev/null +++ b/pos_pms_link/models/pos_payment.py @@ -0,0 +1,36 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2023 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from odoo import api, models + + +class PosPayment(models.Model): + _inherit = "pos.payment" + + @api.constrains("payment_method_id") + def _check_payment_method_id(self): + for payment in self: + if ( + payment.session_id.config_id.pay_on_reservation + and payment.session_id.config_id.pay_on_reservation_method_id + == payment.payment_method_id + ): + continue + else: + super(PosPayment, payment)._check_payment_method_id() diff --git a/pos_pms_link/models/pos_session.py b/pos_pms_link/models/pos_session.py new file mode 100644 index 0000000000..f32a3978a7 --- /dev/null +++ b/pos_pms_link/models/pos_session.py @@ -0,0 +1,378 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2023 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import logging +from collections import defaultdict + +from odoo import _, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class PosSession(models.Model): + _inherit = "pos.session" + + def _load_model(self, model): + ctx = self.env.context.copy() + ctx.update({"pos_user_force": True}) + return super(PosSession, self.with_context(ctx))._load_model(model) + + def _accumulate_amounts(self, data): # noqa: C901 # too-complex + res = super(PosSession, self)._accumulate_amounts(data) + if ( + self.config_id.pay_on_reservation + and self.config_id.pay_on_reservation_method_id + ): + amounts = lambda: {"amount": 0.0, "amount_converted": 0.0} # noqa E731 + tax_amounts = lambda: { # noqa: E731 + "amount": 0.0, + "amount_converted": 0.0, + "base_amount": 0.0, + "base_amount_converted": 0.0, + } + sales = defaultdict(amounts) + taxes = defaultdict(tax_amounts) + rounded_globally = ( + self.company_id.tax_calculation_rounding_method == "round_globally" + ) + + reservation_orders = self.order_ids.filtered(lambda x: x.pms_reservation_id) + + order_taxes = defaultdict(tax_amounts) + for order_line in reservation_orders.lines: + line = self._prepare_line(order_line) + # Combine sales/refund lines + sale_key = ( + # account + line["income_account_id"], + # sign + -1 if line["amount"] < 0 else 1, + # for taxes + tuple( + (tax["id"], tax["account_id"], tax["tax_repartition_line_id"]) + for tax in line["taxes"] + ), + line["base_tags"], + ) + sales[sale_key] = self._update_amounts( + sales[sale_key], {"amount": line["amount"]}, line["date_order"] + ) + # Combine tax lines + for tax in line["taxes"]: + tax_key = ( + tax["account_id"] or line["income_account_id"], + tax["tax_repartition_line_id"], + tax["id"], + tuple(tax["tag_ids"]), + ) + order_taxes[tax_key] = self._update_amounts( + order_taxes[tax_key], + {"amount": tax["amount"], "base_amount": tax["base"]}, + tax["date_order"], + round=not rounded_globally, + ) + for tax_key, amounts in order_taxes.items(): + if rounded_globally: + amounts = self._round_amounts(amounts) + for amount_key, amount in amounts.items(): + taxes[tax_key][amount_key] += amount + + for element, value in dict(res["taxes"]).items(): + if element in taxes: + value["amount"] = value["amount"] - taxes[element]["amount"] + value["amount_converted"] = ( + value["amount_converted"] - taxes[element]["amount_converted"] + ) + value["base_amount"] = ( + value["base_amount"] - taxes[element]["base_amount"] + ) + value["base_amount_converted"] = ( + value["base_amount_converted"] + - taxes[element]["base_amount_converted"] + ) + + for element, value in dict(res["sales"]).items(): + if element in sales: + value["amount"] = value["amount"] - sales[element]["amount"] + value["amount_converted"] = ( + value["amount_converted"] - sales[element]["amount_converted"] + ) + if self.config_id.pay_on_reservation_method_id.split_transactions: + for element, value in dict(res["split_receivables_pay_later"]).items(): + if ( + element.payment_method_id + == self.config_id.pay_on_reservation_method_id + ): + value["amount"] = 0.0 + value["amount_converted"] = 0.0 + + else: + for element, value in dict( + res["combine_receivables_pay_later"] + ).items(): + if element == self.config_id.pay_on_reservation_method_id: + value["amount"] = 0.0 + value["amount_converted"] = 0.0 + return res + + def _pos_ui_models_to_load(self): + result = super()._pos_ui_models_to_load() + if self.config_id.pay_on_reservation: + result.append("pms.reservation") + return result + + def _loader_params_pms_reservation(self): + domain = [ + "|", + ("state", "=", "onboard"), + "&", + ("checkout", "=", fields.Datetime.now().date()), + ("state", "!=", "cancel"), + ] + if self.config_id and self.config_id.reservation_allowed_propertie_ids: + domain.append( + ( + "pms_property_id", + "in", + self.config_id.reservation_allowed_propertie_ids.ids, + ) + ) + return { + "search_params": { + "domain": domain, + "fields": [ + "name", + "id", + "state", + "service_ids", + "partner_name", + "adults", + "children", + "checkin", + "checkout", + "folio_internal_comment", + "rooms", + ], + }, + } + + def _loader_params_pms_service(self): + return { + "search_params": { + "fields": [ + "name", + "id", + "service_line_ids", + "product_id", + "reservation_id", + ], + }, + } + + def _loader_params_pms_service_line(self): + return { + "search_params": { + "fields": [ + "date", + "service_id", + "id", + "product_id", + "day_qty", + "pos_order_line_ids", + ], + }, + } + + def _loader_params_pos_order_line(self): + return { + "search_params": { + "fields": [ + "qty", + "id", + "pms_service_line_id", + ], + }, + } + + def _get_pos_ui_pms_reservation(self, params): + ctx = self.env.context.copy() + ctx.update({"pos_user_force": True}) + + # 1. Obtener las reservas con `search_read` para todos los campos que necesitas + reservations = ( + self.env["pms.reservation"] + .with_context(ctx) + .search_read(**params["search_params"]) + ) + reservation_ids = [r["id"] for r in reservations] + + if not reservations: + return [] + + # 2. Obtener los servicios relacionados con esas reservas + service_params = self._loader_params_pms_service() + service_params["search_params"]["domain"] = [ + ("reservation_id", "in", reservation_ids) + ] + services = ( + self.env["pms.service"] + .with_context(ctx) + .search_read( + service_params["search_params"]["domain"], + fields=service_params["search_params"]["fields"], + ) + ) + service_ids = [s["id"] for s in services] + + # 3. Obtener las líneas de servicio relacionadas con esos servicios + service_line_params = self._loader_params_pms_service_line() + service_line_params["search_params"]["domain"] = [ + ("service_id", "in", service_ids) + ] + service_lines = ( + self.env["pms.service.line"] + .with_context(ctx) + .search_read( + service_line_params["search_params"]["domain"], + fields=service_line_params["search_params"]["fields"], + ) + ) + service_line_ids = [sl["id"] for sl in service_lines] + + # 4. Obtener las líneas de pedido POS relacionadas con esas líneas de servicio + pos_order_line_params = self._loader_params_pos_order_line() + pos_order_line_params["search_params"]["domain"] = [ + ("pms_service_line_id", "in", service_line_ids) + ] + pos_order_lines = ( + self.env["pos.order.line"] + .with_context(ctx) + .search_read( + pos_order_line_params["search_params"]["domain"], + fields=pos_order_line_params["search_params"]["fields"], + ) + ) + + # 5. Agrupar las líneas de pedido por línea de servicio + pos_order_lines_by_service_line = {} + for pos_order_line in pos_order_lines: + service_line_id = pos_order_line["pms_service_line_id"][0] + if service_line_id not in pos_order_lines_by_service_line: + pos_order_lines_by_service_line[service_line_id] = [] + pos_order_lines_by_service_line[service_line_id].append(pos_order_line) + + # 6. Agrupar las líneas de servicio por servicio + service_lines_by_service = {} + for service_line in service_lines: + service_id = service_line["service_id"][0] + if service_id not in service_lines_by_service: + service_lines_by_service[service_id] = [] + service_line["pos_order_lines"] = pos_order_lines_by_service_line.get( + service_line["id"], [] + ) + service_lines_by_service[service_id].append(service_line) + + # 7. Agrupar los servicios por reserva + services_by_reservation = {} + for service in services: + reservation_id = service["reservation_id"][0] + if reservation_id not in services_by_reservation: + services_by_reservation[reservation_id] = [] + service["service_lines"] = service_lines_by_service.get(service["id"], []) + services_by_reservation[reservation_id].append(service) + + # 8. Añadir los servicios dentro de las reservas + for reservation in reservations: + reservation["services"] = services_by_reservation.get(reservation["id"], []) + + return reservations + + # def get_pos_ui_pms_reservation_by_params(self, custom_search_params): + # """ + # :param custom_search_params: a dictionary containing params of a search_read() + # """ + + # ctx = self.env.context.copy() + # ctx.update({"pos_user_force": True}) + # params = self._loader_params_pms_reservation() + # params['search_params'] = {**params['search_params'], **custom_search_params} + # reservations = self.env['pms.reservation'].with_context(ctx).search_read(**params['search_params']) + # reservation_ids = [r["id"] for r in reservations] + # service_params = self._loader_params_pms_service() + # service_params["search_params"]["domain"] = [('reservation_id', 'in', reservation_ids)] + # services = self.env["pms.service"].with_context(ctx).search(service_params["search_params"]["domain"]) + # services_by_reservation = {} + # for reservation_id, service_group in groupby(services, key=lambda service: service.reservation_id): + # reservation_services = self.env['pms.service'].concat(*service_group) + # services_by_reservation[reservation_id.id] = reservation_services.read(service_params['search_params']['fields']) + + # for reservation in reservations: + # reservation['services'] = services_by_reservation.get(reservation['id'], []) + + # return reservations + + def try_cash_in_out(self, _type, amount, reason, extras): + sign = 1 if _type == "in" else -1 + sessions = self.filtered("cash_journal_id") + if not sessions: + raise UserError(_("There is no cash payment method for this PoS Session")) + + partner_id = self.env.context.get("partner_id", False) + self.env["account.bank.statement.line"].sudo().create( + [ + { + "pos_session_id": session.id, + "journal_id": session.cash_journal_id.id, + "amount": sign * amount, + "date": fields.Date.context_today(self), + "payment_ref": "-".join( + [session.name, extras["translatedType"], reason] + ), + "partner_id": partner_id, + } + for session in sessions + ] + ) + cashier = self.env.context.get("cashier", False) + message_content = [f"-Cashier: {cashier}"] if cashier else [] + message_content.append(f'-Cash {extras["translatedType"]}') + message_content.append(f'-Amount: {extras["formattedAmount"]}') + if reason: + message_content.append(f"-Reason: {reason}") + self.message_post(body="
\n".join(message_content)) + + def set_cashbox_pos(self, cashbox_value, notes): + super().set_cashbox_pos(cashbox_value, notes) + cashier = self.env.context.get("cashier", False) + if cashier: + self.message_post( + body=f'Session opened by cashier: {cashier}' + ) + + def close_session_from_ui(self, bank_payment_method_diff_pairs=None): + result = super().close_session_from_ui(bank_payment_method_diff_pairs) + if result.get("successful"): + cashier = self.env.context.get("cashier", False) + if cashier: + self.message_post( + body=f'Session ended by cashier: {cashier}' + ) + return result diff --git a/pos_pms_link/models/product_pricelist.py b/pos_pms_link/models/product_pricelist.py new file mode 100644 index 0000000000..1fb670f64d --- /dev/null +++ b/pos_pms_link/models/product_pricelist.py @@ -0,0 +1,56 @@ +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from odoo import api, models + + +class ProductPricelist(models.Model): + _inherit = "product.pricelist" + + @api.model + def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): + if self.env.context.get("pos_user_force", False): + return ( + super() + .sudo() + .with_context(pos_user_force=False) + .search_read(domain, fields, offset, limit, order) + ) + else: + return super(ProductPricelist, self).search_read( + domain, fields, offset, limit, order + ) + + +class ProductPricelistItem(models.Model): + _inherit = "product.pricelist.item" + + @api.model + def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): + if self.env.context.get("pos_user_force", False): + return ( + super() + .sudo() + .with_context(pos_user_force=False) + .search_read(domain, fields, offset, limit, order) + ) + else: + return super(ProductPricelistItem, self).search_read( + domain, fields, offset, limit, order + ) diff --git a/pos_pms_link/models/res_config_settings.py b/pos_pms_link/models/res_config_settings.py new file mode 100644 index 0000000000..ca3ab87cec --- /dev/null +++ b/pos_pms_link/models/res_config_settings.py @@ -0,0 +1,25 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + + _inherit = "res.config.settings" + + pos_pay_on_reservation = fields.Boolean( + related="pos_config_id.pay_on_reservation", readonly=False + ) + pos_pay_on_reservation_method_id = fields.Many2one( + related="pos_config_id.pay_on_reservation_method_id", readonly=False + ) + pos_reservation_allowed_propertie_ids = fields.Many2many( + related="pos_config_id.reservation_allowed_propertie_ids", readonly=False + ) + pos_close_session_allowed = fields.Boolean( + related="pos_config_id.close_session_allowed", readonly=False + ) + pos_cash_in_out_allowed = fields.Boolean( + related="pos_config_id.cash_in_out_allowed", readonly=False + ) + pos_cash_move_partner = fields.Boolean( + related="pos_config_id.cash_move_partner", readonly=False + ) diff --git a/pos_pms_link/models/res_partner.py b/pos_pms_link/models/res_partner.py new file mode 100644 index 0000000000..72949e832c --- /dev/null +++ b/pos_pms_link/models/res_partner.py @@ -0,0 +1,19 @@ +from odoo import api, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + @api.model + def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): + if self.env.context.get("pos_user_force", False): + return ( + super() + .sudo() + .with_context(pos_user_force=False) + .search_read(domain, fields, offset, limit, order) + ) + else: + return super(ResPartner, self).search_read( + domain, fields, offset, limit, order + ) diff --git a/pos_pms_link/static/description/icon.png b/pos_pms_link/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/pos_pms_link/static/description/icon.png differ diff --git a/pos_pms_link/static/src/js/CashOpeningPopup.js b/pos_pms_link/static/src/js/CashOpeningPopup.js new file mode 100644 index 0000000000..1d286eb9f0 --- /dev/null +++ b/pos_pms_link/static/src/js/CashOpeningPopup.js @@ -0,0 +1,26 @@ +/** @odoo-module **/ + +import Registries from "point_of_sale.Registries"; +import CashOpeningPopup from "point_of_sale.CashOpeningPopup"; +import AbstractAwaitablePopup from 'point_of_sale.AbstractAwaitablePopup'; + + +const PosPmsLinkCashOpeningPopup = (CashOpeningPopup) => + class extends CashOpeningPopup { + async confirm() { + this.env.pos.pos_session.cash_register_balance_start = this.state.openingCash; + this.env.pos.pos_session.state = 'opened'; + var context = this.env.session.user_context; + var chasier = this.env.pos.get_cashier(); + context['cashier'] = chasier.name; + this.rpc({ + model: 'pos.session', + method: 'set_cashbox_pos', + args: [this.env.pos.pos_session.id, this.state.openingCash, this.state.notes], + context: context, + }); + AbstractAwaitablePopup.prototype.confirm.apply(this, arguments); + } + } + +Registries.Component.extend(CashOpeningPopup, PosPmsLinkCashOpeningPopup); \ No newline at end of file diff --git a/pos_pms_link/static/src/js/Popups/CashMovePopup.js b/pos_pms_link/static/src/js/Popups/CashMovePopup.js new file mode 100644 index 0000000000..ec5c9f57e4 --- /dev/null +++ b/pos_pms_link/static/src/js/Popups/CashMovePopup.js @@ -0,0 +1,80 @@ +/** @odoo-module **/ + +import Registries from "point_of_sale.Registries"; +import CashMovePopup from "point_of_sale.CashMovePopup"; + + +const PosPmsLinkCashMovePopup = (CashMovePopup) => + class extends CashMovePopup { + setup() { + super.setup(); + this.state.partner = null; + this.state.no_partner_cash_move = false; + } + + showPartnerButton() { + var cash_move_partner = + this.env.pos && + this.env.pos.config && + this.env.pos.config.cash_control && + this.env.pos.config.cash_move_partner; + return cash_move_partner; + } + + async onClickPartner(ev) { + // IMPROVEMENT: This code snippet is very similar to selectPartner of PaymentScreen. + const currentPartner = this.state.partner; + const {confirmed, payload: newPartner} = await this.showPopup( + "PartnerListPopup", + {partner: currentPartner} + ); + if (confirmed) { + this.state.partner = newPartner; + } + } + + get isLongName() { + return this.state.partner && this.state.partner.name.length > 10; + } + + getPayload() { + var res = super.getPayload(); + res["partner"] = this.state.partner; + return res; + } + + confirm() { + if(this.showPartnerButton()) { + if (this.state.no_partner_cash_move) { + if (!this.state.inputReason) { + this.state.inputHasError = true; + this.errorMessage = this.env._t("Please enter a reason."); + return; + } + } else if (!this.state.partner) { + this.state.inputHasError = true; + this.errorMessage = this.env._t("Select a partner before confirming."); + return; + } + } + return super.confirm(); + } + onClickNoPartnerCashMove(ev) { + this.state.no_partner_cash_move = !this.state.no_partner_cash_move; + var $button = $('#cash-partner'); + if (this.state.no_partner_cash_move) { + this.state.partner = null; + if ($button){ + $button.css('display', 'none'); + } + } + else { + if ($button){ + $button.css('display', 'block'); + } + } + this.state.inputHasError = false; + } + }; + +Registries.Component.extend(CashMovePopup, PosPmsLinkCashMovePopup); diff --git a/pos_pms_link/static/src/js/Popups/ClosePosPopup.js b/pos_pms_link/static/src/js/Popups/ClosePosPopup.js new file mode 100644 index 0000000000..126d2819f1 --- /dev/null +++ b/pos_pms_link/static/src/js/Popups/ClosePosPopup.js @@ -0,0 +1,19 @@ +/** @odoo-module **/ + +import Registries from "point_of_sale.Registries"; +import ClosePosPopup from "point_of_sale.ClosePosPopup"; + + +const PosPmsLinkClosePosPopup = (ClosePosPopup) => + class extends ClosePosPopup { + async closeSession() { + + var context = this.env.session.user_context; + var cashier = this.env.pos.get_cashier(); + context['cashier'] = cashier.name; + this.env.session.userContext = context; + super.closeSession(); + } + } + +Registries.Component.extend(ClosePosPopup, PosPmsLinkClosePosPopup); \ No newline at end of file diff --git a/pos_pms_link/static/src/js/Popups/PartnerListPopup.js b/pos_pms_link/static/src/js/Popups/PartnerListPopup.js new file mode 100644 index 0000000000..3880907bf9 --- /dev/null +++ b/pos_pms_link/static/src/js/Popups/PartnerListPopup.js @@ -0,0 +1,228 @@ +/** @odoo-module **/ + +import AbstractAwaitablePopup from "point_of_sale.AbstractAwaitablePopup"; +import Registries from "point_of_sale.Registries"; +import { isConnectionError } from "point_of_sale.utils"; +import { useListener, useAutofocus } from "@web/core/utils/hooks"; +import { session } from "@web/session"; +import { debounce } from "@web/core/utils/timing"; +import { useAsyncLockedMethod } from "point_of_sale.custom_hooks"; +import { useRef, onWillUnmount } from "@odoo/owl"; + + +class PartnerListPopup extends AbstractAwaitablePopup{ + setup(){ + super.setup(); + useAutofocus({refName: 'search-word-input-partner'}); + useListener('click-save', () => this.env.bus.trigger('save-partner')); + useListener('save-changes', useAsyncLockedMethod(this.saveChanges)); + this.searchWordInputRef = useRef('search-word-input-partner'); + + // We are not using useState here because the object + // passed to useState converts the object and its contents + // to Observer proxy. Not sure of the side-effects of making + // a persistent object, such as pos, into Observer. But it + // is better to be safe. + this.state = { + query: null, + selectedPartner: this.props.partner, + detailIsShown: false, + editModeProps: { + partner: null, + }, + previousQuery: "", + currentOffset: 0, + }; + this.updatePartnerList = debounce(this.updatePartnerList, 70); + onWillUnmount(this.updatePartnerList.cancel); + } + + // Usar cancel() para cerrar el popup si no está en modo edición + cancel() { + if(this.state.detailIsShown) { + this.state.detailIsShown = false; + this.render(true); + } else { + super.cancel(); + } + } + //getPayload() para obtener el payload del popup cuando se confirma + async getPayload() { + return this.state.selectedPartner; + } + + + // confirm() { + // this.props.resolve({ confirmed: true, payload: this.state.selectedPartner }); + // this.trigger('close-temp-screen'); + // } + + activateEditMode() { + this.state.detailIsShown = true; + this.render(true); + } + // Getters + + get partners() { + let res; + if (this.state.query && this.state.query.trim() !== '') { + res = this.env.pos.db.search_partner(this.state.query.trim()); + } else { + res = this.env.pos.db.get_partners_sorted(1000); + } + res.sort(function (a, b) { return (a.name || '').localeCompare(b.name || '') }); + // the selected partner (if any) is displayed at the top of the list + if (this.state.selectedPartner) { + let indexOfSelectedPartner = res.findIndex( partner => + partner.id === this.state.selectedPartner.id + ); + if (indexOfSelectedPartner !== -1) { + res.splice(indexOfSelectedPartner, 1); + } + res.unshift(this.state.selectedPartner); + } + return res + } + get isBalanceDisplayed() { + return false; + } + get partnerLink() { + return `/web#model=res.partner&id=${this.state.editModeProps.partner.id}`; + } + + // Methods + + async _onPressEnterKey() { + if (!this.state.query) return; + const result = await this.searchPartner(); + if (result.length > 0) { + this.showNotification( + _.str.sprintf( + this.env._t('%s customer(s) found for "%s".'), + result.length, + this.state.query + ), + 3000 + ); + } else { + this.showNotification( + _.str.sprintf( + this.env._t('No more customer found for "%s".'), + this.state.query + ), + 3000 + ); + } + + } + _clearSearch() { + this.searchWordInputRef.el.value = ''; + this.state.query = ''; + this.render(true); + } + // We declare this event handler as a debounce function in + // order to lower its trigger rate. + async updatePartnerList(event) { + this.state.query = event.target.value; + this.render(true); + } + clickPartner(partner) { + if (this.state.selectedPartner && this.state.selectedPartner.id === partner.id) { + this.state.selectedPartner = null; + } else { + this.state.selectedPartner = partner; + } + this.confirm(); + } + editPartner(partner) { + this.state.editModeProps.partner = partner; + this.activateEditMode(); + } + createPartner() { + // initialize the edit screen with default details about country, state & lang + this.state.editModeProps.partner = { + country_id: this.env.pos.company.country_id, + state_id: this.env.pos.company.state_id, + lang: session.user_context.lang, + } + this.activateEditMode(); + } + async saveChanges(event) { + try { + let partnerId = await this.rpc({ + model: 'res.partner', + method: 'create_from_ui', + args: [event.detail.processedChanges], + }); + await this.env.pos._loadPartners([partnerId]); + this.state.selectedPartner = this.env.pos.db.get_partner_by_id(partnerId); + this.confirm(); + } catch (error) { + if (isConnectionError(error)) { + await this.showPopup('OfflineErrorPopup', { + title: this.env._t('Offline'), + body: this.env._t('Unable to save changes.'), + }); + } else { + throw error; + } + } + } + async searchPartner() { + if (this.state.previousQuery != this.state.query) { + this.state.currentOffset = 0; + } + let result = await this.getNewPartners(); + this.env.pos.addPartners(result); + this.render(true); + if (this.state.previousQuery == this.state.query) { + this.state.currentOffset += result.length; + } else { + this.state.previousQuery = this.state.query; + this.state.currentOffset = result.length; + } + return result; + } + async getNewPartners() { + let domain = []; + const limit = 30; + if(this.state.query) { + const search_fields = [ + "name", + "parent_name", + "phone_mobile_search", + "email", + "vat", + ]; + domain = [ + ...Array(search_fields.length - 1).fill('|'), + ...search_fields.map(field => [field, "ilike", this.state.query + "%"]) + ]; + } + const result = await this.env.services.rpc( + { + model: 'pos.session', + method: 'get_pos_ui_res_partner_by_params', + args: [ + [odoo.pos_session_id], + { + domain, + limit: limit, + offset: this.state.currentOffset, + }, + ], + context: this.env.session.user_context, + }, + { + timeout: 3000, + shadow: true, + } + ); + return result; + } +} +PartnerListPopup.template = 'PartnerListPopup'; + +Registries.Component.add(PartnerListPopup); + +return PartnerListPopup; \ No newline at end of file diff --git a/pos_pms_link/static/src/js/ReservationSelectionButton.js b/pos_pms_link/static/src/js/ReservationSelectionButton.js new file mode 100644 index 0000000000..82e297d51e --- /dev/null +++ b/pos_pms_link/static/src/js/ReservationSelectionButton.js @@ -0,0 +1,34 @@ +/** @odoo-module **/ + +import Registries from "point_of_sale.Registries"; +import PosComponent from "point_of_sale.PosComponent"; +import ProductScreen from "point_of_sale.ProductScreen"; +import {_t} from "web.core"; + +class ReservationSelectionButton extends PosComponent { + get currentOrder() { + return this.env.pos.get_order(); + } + + async onClick() { + const {confirmed, payload: newReservation} = await this.showTempScreen( + "ReservationListScreen", + {reservation: null} + ); + if (confirmed) { + this.currentOrder.add_reservation_services(newReservation); + console.log(newReservation); + } + } +} + +ReservationSelectionButton.template = "ReservationSelectionButton"; + +ProductScreen.addControlButton({ + component: ReservationSelectionButton, + condition: function () { + return true; + }, +}); + +Registries.Component.add(ReservationSelectionButton); diff --git a/pos_pms_link/static/src/js/Screens/ChromeWidgets/CashMoveButton.js b/pos_pms_link/static/src/js/Screens/ChromeWidgets/CashMoveButton.js new file mode 100644 index 0000000000..91f3954eba --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/ChromeWidgets/CashMoveButton.js @@ -0,0 +1,48 @@ +/** @odoo-module **/ +import Registries from "point_of_sale.Registries"; +import CashMoveButton from "point_of_sale.CashMoveButton"; + + +const PosPmsLinkCashMoveButton = (CashMoveButton) => + class extends CashMoveButton{ + async onClick() { + const {confirmed, payload} = await this.showPopup("CashMovePopup"); + if (!confirmed) return; + const {type, amount, reason, partner} = payload; + const translatedType = this.env._t(type); + const formattedAmount = this.env.pos.format_currency(amount); + if (!amount) { + return this.showNotification( + _.str.sprintf(this.env._t("Cash in/out of %s is ignored."), formattedAmount), + 3000 + ); + } + const cashier = this.env.pos.get_cashier(); + var context = this.env.session.user_context; + // Pasar el nombre del cajero y el id del partner al método try_cash_in_out del modelo pos.session + context['cashier'] = cashier.name; + context['partner_id'] = partner ? partner.id : false; + const extras = {formattedAmount, translatedType}; + await this.rpc({ + model: "pos.session", + method: "try_cash_in_out", + args: [this.env.pos.pos_session.id, type, amount, reason, extras], + context: context, + }); + if (this.env.proxy.printer) { + const renderedReceipt = renderToString("point_of_sale.CashMoveReceipt", { + _receipt: this._getReceiptInfo({...payload, translatedType, formattedAmount}), + }); + const printResult = await this.env.proxy.printer.print_receipt(renderedReceipt); + if (!printResult.successful) { + this.showPopup("ErrorPopup", {title: printResult.message.title, body: printResult.message.body}); + } + } + this.showNotification( + _.str.sprintf(this.env._t("Successfully made a cash %s of %s."), type, formattedAmount), + 3000 + ); + } + } + +Registries.Component.extend(CashMoveButton, PosPmsLinkCashMoveButton); \ No newline at end of file diff --git a/pos_pms_link/static/src/js/Screens/ChromeWidgets/Chrome.js b/pos_pms_link/static/src/js/Screens/ChromeWidgets/Chrome.js new file mode 100644 index 0000000000..ede725d078 --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/ChromeWidgets/Chrome.js @@ -0,0 +1,23 @@ +/** @odoo-module **/ +import Registries from "point_of_sale.Registries"; +import Chrome from "point_of_sale.Chrome"; + +const PosPmsLinkChrome = (Chrome) => + class extends Chrome { + get headerButtonIsShown() { + var showButton = super.headerButtonIsShown; + var close_session_allowed = this.env.pos.config.close_session_allowed; + return close_session_allowed ? close_session_allowed : showButton; + } + showCashMoveButton() { + var showButton = super.showCashMoveButton(); + var cash_in_out_allowed = + this.env.pos && + this.env.pos.config && + this.env.pos.config.cash_control && + this.env.pos.config.cash_in_out_allowed; + return cash_in_out_allowed ? cash_in_out_allowed : showButton; + } + }; + +Registries.Component.extend(Chrome, PosPmsLinkChrome); diff --git a/pos_pms_link/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/pos_pms_link/static/src/js/Screens/PaymentScreen/PaymentScreen.js new file mode 100644 index 0000000000..3d758cf3f8 --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -0,0 +1,46 @@ +/** @odoo-module **/ + +import PaymentScreen from "point_of_sale.PaymentScreen"; +import Registries from "point_of_sale.Registries"; + +const PosPMSLinkPaymentScreen = (PaymentScreen) => + class extends PaymentScreen { + async selectReservation() { + const {confirmed, payload: newReservation} = await this.showTempScreen( + "ReservationListScreen", + { + reservation: null, + } + ); + if (confirmed) { + var self = this; + + const {confirmed} = await this.showPopup("ConfirmPopup", { + title: this.env._t("Pay order with reservation ?"), + body: this.env._t( + "This operation will add all the products in the order to the reservation. RESERVATION: " + + newReservation.name + + " PARTNER : " + + newReservation.partner_name + + " ROOM: " + + newReservation.rooms + ), + }); + if (confirmed) { + var payment_method = { + id: self.env.pos.config.pay_on_reservation_method_id[0], + name: self.env.pos.config.pay_on_reservation_method_id[1], + is_cash_count: false, + pos_mercury_config_id: false, + use_payment_terminal: false, + }; + self.trigger("new-payment-line", payment_method); + this.currentOrder.set_paid_on_reservation(true); + this.currentOrder.set_pms_reservation_id(newReservation.id); + self.validateOrder(false); + } + } + } + }; + +Registries.Component.extend(PaymentScreen, PosPMSLinkPaymentScreen); diff --git a/pos_pms_link/static/src/js/Screens/ReceiptScreen/OrderReceipt.js b/pos_pms_link/static/src/js/Screens/ReceiptScreen/OrderReceipt.js new file mode 100644 index 0000000000..7573c23629 --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/ReceiptScreen/OrderReceipt.js @@ -0,0 +1,21 @@ +/** @odoo-module **/ + +import OrderReceipt from "point_of_sale.OrderReceipt"; +import Registries from "point_of_sale.Registries"; + +const PosPMSLinkOrderReceipt = (OrderReceipt) => + class extends OrderReceipt { + get paid_on_reservation() { + return this._receiptEnv.receipt.paid_on_reservation; + } + + get reservation_name() { + return ( + this.env.pos.db.get_reservation_by_id( + this._receiptEnv.receipt.pms_reservation_id + ).partner_name || "" + ); + } + }; + +Registries.Component.extend(OrderReceipt, PosPMSLinkOrderReceipt); diff --git a/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationDetailsEdit.js b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationDetailsEdit.js new file mode 100644 index 0000000000..21f86a9c73 --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationDetailsEdit.js @@ -0,0 +1,21 @@ +/** @odoo-module **/ + +import PosComponent from "point_of_sale.PosComponent"; +import Registries from "point_of_sale.Registries"; + +class ReservationDetailsEdit extends PosComponent { + setup() { + super.setup(); + const reservation = this.props.reservation; + // onMounted(() => { + // this.env.bus.on("save-reservation", this, this.saveChanges); + // }); + // onWillUnmount(() => { + // this.env.bus.off("save-reservation", this); + // }); + } +} + +ReservationDetailsEdit.template = "ReservationDetailsEdit"; + +Registries.Component.add(ReservationDetailsEdit); diff --git a/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationLine.js b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationLine.js new file mode 100644 index 0000000000..acb6d44cfb --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationLine.js @@ -0,0 +1,16 @@ +/** @odoo-module **/ + +import PosComponent from "point_of_sale.PosComponent"; +import Registries from "point_of_sale.Registries"; + +class ReservationLine extends PosComponent { + get highlight() { + return this.props.reservation !== this.props.selectedReservation + ? "" + : "highlight"; + } +} + +ReservationLine.template = "ReservationLine"; + +Registries.Component.add(ReservationLine); diff --git a/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationListScreen.js b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationListScreen.js new file mode 100644 index 0000000000..882156b2b9 --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationListScreen.js @@ -0,0 +1,179 @@ +/** @odoo-module **/ + +import PosComponent from "point_of_sale.PosComponent"; +import Registries from "point_of_sale.Registries"; +import {useAsyncLockedMethod} from "point_of_sale.custom_hooks"; +import {useListener, useAutofocus} from "@web/core/utils/hooks"; +import {onWillUnmount, useRef} from "@odoo/owl"; +import {debounce} from "@web/core/utils/timing"; +import {_t} from "web.core"; + +class ReservationListScreen extends PosComponent { + setup() { + super.setup(); + useAutofocus({refName: "search-word-input-reservation"}); + // useListener("click-save", () => this.env.bus.trigger("save-partner")); + // useListener("click-edit", () => this.editReservation()); + useListener("save-changes", useAsyncLockedMethod(this.saveChanges)); + this.searchWordInputRef = useRef("search-word-input-reservation"); + + // We are not using useState here because the object + // passed to useState converts the object and its contents + // to Observer proxy. Not sure of the side-effects of making + // a persistent object, such as pos, into Observer. But it + // is better to be safe. + this.state = { + query: null, + selectedReservation: this.props.reservation, + detailIsShown: false, + isEditMode: false, + editModeProps: { + reservation: null, + }, + previousQuery: "", + currentOffset: 0, + }; + this.updateReservationList = debounce(this.updateReservationList, 70); + onWillUnmount(this.updateReservationList.cancel); + } + + // Lifecycle hooks + back() { + if (this.state.detailIsShown) { + this.state.detailIsShown = false; + this.render(true); + } else { + this.props.resolve({confirmed: false, payload: false}); + this.trigger("close-temp-screen"); + } + } + confirm() { + this.props.resolve({confirmed: true, payload: this.state.selectedReservation}); + this.trigger("close-temp-screen"); + } + + // Getters + + get currentOrder() { + return this.env.pos.get_order(); + } + + get reservations() { + if (this.state.query && this.state.query.trim() !== "") { + return this.env.pos.db.search_reservation(this.state.query.trim()); + } + return this.env.pos.db.get_reservations_sorted(1000); + } + + get isNextButtonVisible() { + return Boolean(this.state.selectedReservation); + } + /** + * Returns the text and command of the next button. + * The command field is used by the clickNext call. + */ + get nextButton() { + if (!this.props.reservation) { + return {command: "set", text: this.env._t("Set Reservation")}; + } else if ( + this.props.reservation && + this.props.reservation === this.state.selectedReservation + ) { + return {command: "deselect", text: this.env._t("Deselect Reservation")}; + } + return {command: "set", text: this.env._t("Change Reservation")}; + } + + clickNext() { + this.state.selectedReservation = + this.nextButton.command === "set" ? this.state.selectedReservation : null; + this.confirm(); + } + // Methods + + _clearSearch() { + this.searchWordInputRef.el.value = ""; + this.state.query = ""; + this.render(true); + } + + // We declare this event handler as a debounce function in + // order to lower its trigger rate. + async updateReservationList(event) { + this.state.query = event.target.value; + this.render(true); + } + + clickReservation(reservation) { + if (this.state.selectedReservation === reservation) { + this.state.selectedCReservation = null; + } else { + this.state.selectedReservation = reservation; + } + this.render(true); + } + + editReservation(reservation) { + this.state.editModeProps.reservation = reservation; + this.activateEditMode(); + } + + activateEditMode() { + this.state.detailIsShown = true; + this.render(true); + } + + deactivateEditMode() { + this.state.isEditMode = false; + this.state.editModeProps = { + reservation: {}, + }; + this.render(); + } + cancelEdit() { + this.deactivateEditMode(); + } + + async saveChanges(event) { + try { + let reservartionId = await this.rpc({ + model: "pm.reservation", + method: "create_from_ui", + args: [event.detail.processedChanges], + }); + await this.env.pos._load([reservartionId]); + this.state.selectedReservation = + this.env.pos.db.get_reservation_by_id(reservartionId); + this.confirm(); + } catch (error) { + if (isConnectionError(error)) { + await this.showPopup("OfflineErrorPopup", { + title: this.env._t("Offline"), + body: this.env._t("Unable to save changes."), + }); + } else { + throw error; + } + } + } + + async searchReservation() { + if (this.state.previousQuery != this.state.query) { + this.state.currentOffset = 0; + } + let result = await this.getNewReservations(); + this.env.pos.addReservations(result); + this.render(true); + if (this.state.previousQuery == this.state.query) { + this.state.currentOffset += result.length; + } else { + this.state.previousQuery = this.state.query; + this.state.currentOffset = result.length; + } + return result; + } +} + +ReservationListScreen.template = "ReservationListScreen"; + +Registries.Component.add(ReservationListScreen); diff --git a/pos_pms_link/static/src/js/db.js b/pos_pms_link/static/src/js/db.js new file mode 100644 index 0000000000..ca6b6987f2 --- /dev/null +++ b/pos_pms_link/static/src/js/db.js @@ -0,0 +1,131 @@ +/** @odoo-module **/ +/* +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +*/ + +import PosDB from "point_of_sale.DB"; +import {patch, unaccent} from "web.utils"; + +patch(PosDB.prototype, "pos_pms_link.PosDB", { + init(options) { + this._super(options); + this.reservation_sorted = []; + this.reservation_by_id = {}; + // this.reservation_search_string = ""; + this.reservation_search_strings = {}; + this.reservation_id = null; + }, + get_reservation_by_id(id) { + return this.reservation_by_id[id]; + }, + get_reservations_sorted(max_count) { + max_count = max_count + ? Math.min(this.reservation_sorted.length, max_count) + : this.reservation_sorted.length; + var reservations = []; + for (var i = 0; i < max_count; i++) { + reservations.push(this.reservation_by_id[this.reservation_sorted[i]]); + } + return reservations; + }, + search_reservation(query) { + try { + query = query.replace( + /[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g, + "." + ); + query = query.replace(/ /g, ".+"); + var re = RegExp("([0-9]+):.*?" + unaccent(query), "gi"); + } catch (e) { + return []; + } + var results = []; + const searchStrings = Object.values(this.reservation_search_strings).reverse(); + let searchString = searchStrings.pop(); + while (searchString && results.length < this.limit) { + var r = re.exec(searchString); + if (r) { + var id = Number(r[1]); + var res = this.get_reservation_by_id(id); + if (res) { + results.push(res); + } + } else { + searchString = searchStrings.pop(); + } + } + return results; + }, + _reservation_search_string(reservation) { + var str = reservation.name || ""; + var room_str = reservation.rooms || ""; + var partner_str = reservation.partner_name || ""; + str = + String(reservation.id) + + ":" + + str.replace(":", "").replace(/\n/g, " ") + + ":" + + room_str.replace(":", "").replace(/\n/g, " ") + + ":" + + partner_str.replace(":", "").replace(/\n/g, " ") + + "\n"; + return str; + }, + add_reservations(reservations) { + var updated = {}; + var reservation; + for (var i = 0, len = reservations.length; i < len; i++) { + reservation = reservations[i]; + + if (!this.reservation_by_id[reservation.id]) { + this.reservation_sorted.push(reservation.id); + } else { + const oldReservation = this.reservation_by_id[reservation.id]; + } + updated[reservation.id] = reservation; + this.reservation_by_id[reservation.id] = reservation; + } + + const updatedChunks = new Set(); + const CHUNK_SIZE = 100; + for (const id in updated) { + const chunkId = Math.floor(id / CHUNK_SIZE); + if (updatedChunks.has(chunkId)) { + // another reservation in this chunk was updated and we already rebuild the chunk + continue; + } + updatedChunks.add(chunkId); + // If there were updates, we need to rebuild the search string for this chunk + let searchString = ""; + + for (let id = chunkId * CHUNK_SIZE; id < (chunkId + 1) * CHUNK_SIZE; id++) { + if (!(id in this.reservation_by_id)) { + continue; + } + const reservation = this.reservation_by_id[id]; + searchString += this._reservation_search_string(reservation); + } + + this.reservation_search_strings[chunkId] = unaccent(searchString); + } + return Object.keys(updated).length; + }, +}); diff --git a/pos_pms_link/static/src/js/models.js b/pos_pms_link/static/src/js/models.js new file mode 100644 index 0000000000..d27ccb1a7c --- /dev/null +++ b/pos_pms_link/static/src/js/models.js @@ -0,0 +1,352 @@ +/** @odoo-module **/ + +/* +############################################################################## +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved +# Vicente Ángel Gutiérrez +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +*/ + +import Registries from "point_of_sale.Registries"; +import {PosGlobalState, Order, Orderline} from "point_of_sale.models"; + +const PosPmsGlobalState = (PosGlobalState) => + class extends PosGlobalState { + constructor(obj) { + super(obj); + } + + //@override + async _processData(loadedData) { + await super._processData(...arguments); + if (this.config.pay_on_reservation) { + this.reservations = loadedData["pms.reservation"]; + this.loadPmsReservation(); + this.addReservations(this.reservations); + } + } + + loadPmsReservation() { + if (this.config.pay_on_reservation) { + this.reservations_by_id = {}; + this.services_by_id = {}; + for (let reservation of this.reservations) { + this.reservations_by_id[reservation.id] = reservation; + for (let service of reservation.services) { + this.services_by_id[service.id] = service; + service.reservation = reservation; + } + } + } + } + + async _loadReservations(reservartionIds) { + if (reservartionIds.lenght > 0) { + var domain = [["id", "in", reservartionIds]]; + const fetchedReservations = await this.env.services.rpc( + { + model: "pos.session", + method: "get_pos_ui_pms_reservation_by_params", + args: [[odoo.pos_session_id], {domain}], + }, + { + timeout: 3000, + shadow: true, + } + ); + this.addReservations(fetchedReservations); + } + } + + addReservations(reservations) { + return this.db.add_reservations(reservations); + } + }; + +Registries.Model.extend(PosGlobalState, PosPmsGlobalState); + +const PosPmsOrder = (Order) => + class extends Order { + constructor(obj, options) { + super(obj, options); + this.paid_on_reservation = this.paid_on_reservation || null; + this.pms_reservation_id = this.pms_reservation_id || null; + } + + get_paid_on_reservation() { + return this.paid_on_reservation; + } + + set_paid_on_reservation(value) { + this.paid_on_reservation = value; + } + + get_pms_reservation_id() { + return this.pms_reservation_id; + } + + set_pms_reservation_id(value) { + this.pms_reservation_id = value; + } + + export_as_JSON() { + var json = super.export_as_JSON(); + json.paid_on_reservation = this.paid_on_reservation; + json.pms_reservation_id = this.pms_reservation_id; + return json; + } + + init_from_JSON(json) { + super.init_from_JSON(json); + this.paid_on_reservation = json.paid_on_reservation; + this.pms_reservation_id = json.pms_reservation_id; + } + + apply_ms_data(data) { + if (typeof data.paid_on_reservation !== "undefined") { + this.set_paid_on_reservation(data.paid_on_reservation); + } + if (typeof data.pms_reservation_id !== "undefined") { + this.set_pms_reservation_id(data.pms_reservation_id); + } + this.trigger("change", this); + } + + add_reservation_services(reservation) { + var self = this; + var d = new Date(); + var month = d.getMonth() + 1; + var day = d.getDate(); + + var current_date = + d.getFullYear() + + "-" + + (month < 10 ? "0" : "") + + month + + "-" + + (day < 10 ? "0" : "") + + day; + + var service_lines = + reservation.services.map((x) => x.service_lines) || false; + var today_service_lines = []; + _.each(service_lines, function (service_array) { + today_service_lines.push( + service_array.find((x) => x.date === current_date) + ); + }); + + _.each(today_service_lines, function (service_line_id) { + if (service_line_id) { + var qty = service_line_id.day_qty; + if (service_line_id.pos_order_lines.length > 0) { + _.each( + service_line_id.pos_order_lines, + function (order_line_id) { + qty -= order_line_id.qty; + } + ); + } + if (qty > 0) { + var options = { + quantity: qty, + pms_service_line_id: service_line_id.id, + price: 0.0, + }; + var service_product = self.pos.db.get_product_by_id( + service_line_id.product_id[0] + ); + self.pos.get_order().add_product(service_product, options); + var last_line = self.pos.get_order().get_last_orderline(); + if (last_line) { + last_line.set_note( + "RESERVATION: " + + reservation.name + + " ROOMS: " + + reservation.rooms + ); + } + var r_service_line_id = reservation.services + .map((x) => x.service_lines)[0] + .find((x) => x.id == service_line_id.id); + if ( + r_service_line_id && + r_service_line_id.pos_order_lines.length == 0 + ) { + r_service_line_id.pos_order_lines.push({ + id: 0, + qty: parseInt(qty), + }); + } else if ( + r_service_line_id && + r_service_line_id.pos_order_lines.length == 1 && + r_service_line_id.pos_order_lines[0].id == 0 + ) { + r_service_line_id.pos_order_lines[0].qty = parseInt(qty); + } else if ( + r_service_line_id && + r_service_line_id.pos_order_lines.length == 1 && + r_service_line_id.pos_order_lines[0].id != 0 + ) { + r_service_line_id.pos_order_lines.push({ + id: 0, + qty: parseInt(qty), + }); + } else if ( + r_service_line_id && + r_service_line_id.pos_order_lines.length > 1 + ) { + var id_in_lines = false; + _.each( + r_service_line_id.pos_order_lines, + function (pos_line_id) { + if (pos_line_id.id == self.id) { + pos_line_id.qty = parseInt(qty); + id_in_lines = true; + } + } + ); + if (id_in_lines == false) { + r_service_line_id.pos_order_lines.push({ + id: self.id, + qty: parseInt(qty), + }); + } + } + } + } + }); + } + + add_product(product, options) { + super.add_product(...arguments); + if (options.pms_service_line_id) { + this.selected_orderline.set_pms_service_line_id( + options.pms_service_line_id + ); + } + } + + export_for_printing() { + var res = super.export_for_printing(); + res.paid_on_reservation = this.paid_on_reservation; + res.pms_reservation_id = this.pms_reservation_id; + return res; + } + }; + +Registries.Model.extend(Order, PosPmsOrder); + +const PosPmsOrderline = (Orderline) => + class extends Orderline { + constructor(obj, options) { + super(obj, options); + this.server_id = this.server_id || null; + this.pms_service_line_id = this.pms_service_line_id || null; + } + + get_pms_service_line_id() { + return this.pms_service_line_id; + } + + set_pms_service_line_id(value) { + this.pms_service_line_id = value; + } + + export_as_JSON() { + var json = super.export_as_JSON(); + json.pms_service_line_id = this.pms_service_line_id; + return json; + } + + init_from_JSON(json) { + super.init_from_JSON(json); + this.pms_service_line_id = json.pms_service_line_id; + this.server_id = json.server_id; + } + + apply_ms_data(data) { + if (typeof data.pms_service_line_id !== "undefined") { + this.set_pms_service_line_id(data.pms_service_line_id); + } + this.trigger("change", this); + } + + set_quantity(quantity, keep_price) { + var res = super.set_quantity(quantity, keep_price); + var is_real_qty = true; + if (!quantity || quantity == "remove") { + is_real_qty = false; + } + var self = this; + if (self.pms_service_line_id) { + this.pos.reservations.map(function (x) { + _.each(x.services, function (service) { + _.each(service.service_lines, function (line) { + if (line.id == self.pms_service_line_id) { + // Si no hay líneas de pedido y la cantidad es real, agregamos una nueva + if (line.pos_order_lines.length == 0 && is_real_qty) { + line.pos_order_lines.push({ + id: self.server_id || 0, + qty: parseInt(quantity), + }); + } + // Si ya existe una línea de pedido con el mismo ID + else if ( + line.pos_order_lines.length == 1 && + line.pos_order_lines[0].id == self.server_id + ) { + if (is_real_qty) { + // Actualizamos la cantidad + line.pos_order_lines[0].qty = + parseInt(quantity); + } else { + // Eliminamos la línea con splice() en lugar de pop() + line.pos_order_lines.splice(0, 1); + } + } + // Si hay varias líneas, buscamos por ID y eliminamos correctamente + else if (line.pos_order_lines.length > 1) { + var index_to_remove = -1; + _.each( + line.pos_order_lines, + function (pos_line_id, index) { + if (pos_line_id.id == self.server_id) { + if (is_real_qty) { + pos_line_id.qty = + parseInt(quantity); + } else { + index_to_remove = index; + } + } + } + ); + if (index_to_remove !== -1) { + line.pos_order_lines.splice(index_to_remove, 1); + } + } + } + }); + }); + }); + } + return res; + } + }; + +Registries.Model.extend(Orderline, PosPmsOrderline); diff --git a/pos_pms_link/static/src/scss/partner_popup.scss b/pos_pms_link/static/src/scss/partner_popup.scss new file mode 100644 index 0000000000..17f185bbf5 --- /dev/null +++ b/pos_pms_link/static/src/scss/partner_popup.scss @@ -0,0 +1,40 @@ +.cash-partner-container{ + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; + gap: 10px; + .button{ + min-width: 30%; + border-radius: 5px; + padding: 0; + } +} +#partner-popup{ + &.screen{ + top: 5% !important; + } +} +#no-partner-move{ + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; + gap: 10px; + .checkbox { + min-height: 0px; + height: 20px; + border: none; + box-shadow: none; + display: inline-block; + width: fit-content; + } + /* Estilos para la etiqueta */ + .custom-label { + font-size: 14px; + color: #444; + margin-left: 8px; + cursor: pointer; + text-transform: uppercase; + } +} \ No newline at end of file diff --git a/pos_pms_link/static/src/xml/Popups/CashMovePopup.xml b/pos_pms_link/static/src/xml/Popups/CashMovePopup.xml new file mode 100644 index 0000000000..7f7a5e620f --- /dev/null +++ b/pos_pms_link/static/src/xml/Popups/CashMovePopup.xml @@ -0,0 +1,36 @@ + + + + +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
\ No newline at end of file diff --git a/pos_pms_link/static/src/xml/Popups/PartnerListPopup.xml b/pos_pms_link/static/src/xml/Popups/PartnerListPopup.xml new file mode 100644 index 0000000000..dbf6b67f11 --- /dev/null +++ b/pos_pms_link/static/src/xml/Popups/PartnerListPopup.xml @@ -0,0 +1,85 @@ + + + + +
+
+
+
+ + + Save + +
+ +
+ + Discard +
+
+ +
+ +
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
NameAddressContactZIPBalance
+
+
Search more
+
+
+
+
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/pos_pms_link/static/src/xml/ReservationSelectionButton.xml b/pos_pms_link/static/src/xml/ReservationSelectionButton.xml new file mode 100644 index 0000000000..bb6d220faf --- /dev/null +++ b/pos_pms_link/static/src/xml/ReservationSelectionButton.xml @@ -0,0 +1,12 @@ + + + + +
+ + + Reservation # +
+
+ +
diff --git a/pos_pms_link/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml b/pos_pms_link/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml new file mode 100644 index 0000000000..ef9c1e271c --- /dev/null +++ b/pos_pms_link/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml @@ -0,0 +1,21 @@ + + + + + +
+
Reservation
+
+
+
+ +
diff --git a/pos_pms_link/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml b/pos_pms_link/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml new file mode 100644 index 0000000000..2b8197b32d --- /dev/null +++ b/pos_pms_link/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml @@ -0,0 +1,22 @@ + + + + + + +

+
+ Signature:

+
+ ------------------- +
+
+
+
+ +
diff --git a/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationDetailsEdit.xml b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationDetailsEdit.xml new file mode 100644 index 0000000000..c2b3e425f8 --- /dev/null +++ b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationDetailsEdit.xml @@ -0,0 +1,79 @@ + + + + +
+

+
+
+ Name:
+ Checkin:
+ Checkout:
+ Adults:
+ Children:
+ Internal comment:
+
+

Services:

+
+ + + + + + + + + + + + + + +
ServiceLines
+ +
    + +
  • + - - +
  • +
    +
+
+
+
+

+
+ +
diff --git a/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationLine.xml b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationLine.xml new file mode 100644 index 0000000000..759e70a1bc --- /dev/null +++ b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationLine.xml @@ -0,0 +1,40 @@ + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
diff --git a/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationListScreen.xml b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationListScreen.xml new file mode 100644 index 0000000000..0d3c436c6d --- /dev/null +++ b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationListScreen.xml @@ -0,0 +1,105 @@ + + + + +
+
+
+ +
+ Discard + + + +
+
+ +
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
NamePartner nameRoomCheckinCheckoutAdultsChildren
+
+
+
+
+
+
+
+
+ +
diff --git a/pos_pms_link/views/assets_common.xml b/pos_pms_link/views/assets_common.xml new file mode 100644 index 0000000000..e7ffe0105d --- /dev/null +++ b/pos_pms_link/views/assets_common.xml @@ -0,0 +1,36 @@ + + + +