diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 38b0ba110e..6578458712 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -19,7 +19,7 @@ jobs: 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/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ee9b2001a..d3eef51355 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -93,7 +93,7 @@ repos: - --color - --fix - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v5.0.0 hooks: - id: trailing-whitespace # exclude autogenerated files @@ -138,11 +138,11 @@ 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 - additional_dependencies: ["flake8-bugbear==21.9.2"] + additional_dependencies: ["flake8-bugbear==21.9.2", "importlib-metadata<5.0.0"] - repo: https://github.com/OCA/pylint-odoo rev: v8.0.19 hooks: diff --git a/account_asset_pms/i18n/it.po b/account_asset_pms/i18n/it.po new file mode 100644 index 0000000000..8cdf449c5f --- /dev/null +++ b/account_asset_pms/i18n/it.po @@ -0,0 +1,70 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_asset_pms +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: account_asset_pms +#: model:ir.model,name:account_asset_pms.model_account_asset +msgid "Asset" +msgstr "" + +#. module: account_asset_pms +#: model:ir.model,name:account_asset_pms.model_account_asset_line +msgid "Asset depreciation table line" +msgstr "" + +#. module: account_asset_pms +#: model:ir.model.fields,field_description:account_asset_pms.field_account_asset__display_name +#: model:ir.model.fields,field_description:account_asset_pms.field_account_asset_line__display_name +#: model:ir.model.fields,field_description:account_asset_pms.field_account_move__display_name +#: model:ir.model.fields,field_description:account_asset_pms.field_report_account_asset_management_asset_report_xls__display_name +msgid "Display Name" +msgstr "" + +#. module: account_asset_pms +#: model:ir.model,name:account_asset_pms.model_report_account_asset_management_asset_report_xls +msgid "Dynamic XLS asset report generator" +msgstr "" + +#. module: account_asset_pms +#: model:ir.model.fields,field_description:account_asset_pms.field_account_asset__id +#: model:ir.model.fields,field_description:account_asset_pms.field_account_asset_line__id +#: model:ir.model.fields,field_description:account_asset_pms.field_account_move__id +#: model:ir.model.fields,field_description:account_asset_pms.field_report_account_asset_management_asset_report_xls__id +msgid "ID" +msgstr "" + +#. module: account_asset_pms +#: model:ir.model,name:account_asset_pms.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: account_asset_pms +#: model:ir.model.fields,field_description:account_asset_pms.field_account_asset____last_update +#: model:ir.model.fields,field_description:account_asset_pms.field_account_asset_line____last_update +#: model:ir.model.fields,field_description:account_asset_pms.field_account_move____last_update +#: model:ir.model.fields,field_description:account_asset_pms.field_report_account_asset_management_asset_report_xls____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_asset_pms +#: code:addons/account_asset_pms/report/account_asset_report_xls.py:0 +#, python-format +msgid "PMS Property" +msgstr "" + +#. module: account_asset_pms +#: model:ir.model.fields,field_description:account_asset_pms.field_account_asset__pms_property_id +msgid "Pms Property" +msgstr "" diff --git a/connector_pms/README.rst b/connector_pms/README.rst new file mode 100644 index 0000000000..ff82411b9a --- /dev/null +++ b/connector_pms/README.rst @@ -0,0 +1,91 @@ +============= +PMS Connector +============= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6c6f3e6839947ed86924adc3667235de9205ec86e0ee48df315f979d5a392cbd + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |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/connector_pms + :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-connector_pms + :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| + +Base module for implement channel connectors + +Features: + + * Avaliability Management + * Odoo Connector + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +No configuration required. This is a 'tool' module, need be used with other modules. + +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 +~~~~~~~ + +* Eric Antones + +Contributors +~~~~~~~~~~~~ + +* Eric Antones + +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/connector_pms/__init__.py b/connector_pms/__init__.py new file mode 100644 index 0000000000..f41943ed22 --- /dev/null +++ b/connector_pms/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import components_custom +from . import components +from . import models diff --git a/connector_pms/__manifest__.py b/connector_pms/__manifest__.py new file mode 100644 index 0000000000..0f1bd2870a --- /dev/null +++ b/connector_pms/__manifest__.py @@ -0,0 +1,37 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "PMS Connector", + "summary": "Channel PMS connector Base", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "development_status": "Alpha", + "category": "Connector", + "website": "https://github.com/OCA/pms", + "author": "Eric Antones ,Odoo Community Association (OCA)", + "depends": [ + "connector", + "pms", + ], + "data": [ + "data/queue_data.xml", + "security/ir.model.access.csv", + "views/channel_menus.xml", + "views/channel_backend_views.xml", + "views/channel_backend_type_views.xml", + "views/channel_backend_log_views.xml", + "views/channel_backend_method_views.xml", + "views/pms_property_views.xml", + "views/pms_room_type_views.xml", + "views/pms_room_type_class_views.xml", + "views/pms_board_service_views.xml", + "views/pms_folio_views.xml", + "views/pms_reservation_views.xml", + "views/product_pricelist_views.xml", + "views/product_pricelist_item_views.xml", + "views/pms_availability_plan_views.xml", + "views/pms_availability_plan_rule_views.xml", + "views/queue_job_views.xml", + ], +} diff --git a/connector_pms/components/__init__.py b/connector_pms/components/__init__.py new file mode 100644 index 0000000000..8d82aaa150 --- /dev/null +++ b/connector_pms/components/__init__.py @@ -0,0 +1,11 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import core +from . import adapter +from . import binder +from . import deleter +from . import exporter +from . import importer +from . import mapper_export +from . import mapper_import diff --git a/connector_pms/components/adapter.py b/connector_pms/components/adapter.py new file mode 100644 index 0000000000..3c27c9cd6a --- /dev/null +++ b/connector_pms/components/adapter.py @@ -0,0 +1,111 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import datetime + +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import AbstractComponent + + +class ChannelAdapter(AbstractComponent): + _name = "channel.adapter" + _inherit = "base.backend.adapter.crud" + + def chunks(self, lst, n): + """Yield successive n-sized chunks from lst.""" + for i in range(0, len(lst), n): + yield lst[i : i + n] + + def _filter(self, values, domain=None): + # TODO support for domains with 'or' clauses + # TODO refactor and optimize + if not domain: + return values + + operations = { + "=": lambda x, y: x != y, + "!=": lambda x, y: x == y, + ">": lambda x, y: x <= y, + "<": lambda x, y: x >= y, + ">=": lambda x, y: x < y, + "<=": lambda x, y: x > y, + } + + values_filtered = [] + for record in values: + for elem in domain: + k, op, v = elem + if k not in record: + raise ValidationError(_("Key %s does not exist") % k) + if operations[op](record[k], v): + break + elif op == "in": + if not isinstance(v, (tuple, list)): + raise ValidationError( + _("The value %s should be a list or tuple") % v + ) + if record[k] not in v: + break + elif op == "not in": + if not isinstance(v, (tuple, list)): + raise ValidationError( + _("The value %s should be a list or tuple") % v + ) + if record[k] in v: + break + else: + break + # raise NotImplementedError("Operator '%s' not supported" % op) + else: + values_filtered.append(record) + + return values_filtered + + def _extract_domain_clauses(self, domain, fields): + if not isinstance(fields, (tuple, list)): + fields = [fields] + extracted, rest = [], [] + for clause in domain: + tgt = extracted if clause[0] in fields else rest + tgt.append(clause) + return extracted, rest + + def _convert_format(self, elem, mapper, path=""): + if isinstance(elem, dict): + for k, v in elem.items(): + current_path = "{}/{}".format(path, k) + if v == "": + elem[k] = None + continue + if isinstance(v, (tuple, list, dict)): + if isinstance(v, dict): + if current_path in mapper: + v2 = {} + for k1, v1 in v.items(): + new_value = mapper[current_path](k1) + v2[new_value] = v1 + v = elem[k] = v2 + self._convert_format(v, mapper, current_path) + elif isinstance( + v, (str, int, float, bool, datetime.date, datetime.datetime) + ): + if current_path in mapper: + elem[k] = mapper[current_path](v) + else: + raise NotImplementedError("Type %s not implemented" % type(v)) + elif isinstance(elem, (tuple, list)): + for ch in elem: + self._convert_format(ch, mapper, path) + elif isinstance( + elem, (str, int, float, bool, datetime.date, datetime.datetime) + ): + pass + else: + raise NotImplementedError("Type %s not implemented" % type(elem)) + + +class ChannelAdapterError(Exception): + def __init__(self, message, data=None): + super().__init__(message) + self.data = data or {} diff --git a/connector_pms/components/binder.py b/connector_pms/components/binder.py new file mode 100644 index 0000000000..af96f53f76 --- /dev/null +++ b/connector_pms/components/binder.py @@ -0,0 +1,8 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.addons.component.core import AbstractComponent + + +class ChannelBinder(AbstractComponent): + _name = "channel.binder" + _inherit = "base.binder.custom" diff --git a/connector_pms/components/core.py b/connector_pms/components/core.py new file mode 100644 index 0000000000..d49ef66e1d --- /dev/null +++ b/connector_pms/components/core.py @@ -0,0 +1,11 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import AbstractComponent + + +class BaseChannelConnector(AbstractComponent): + _name = "base.channel.connector" + _inherit = "base.connector" + + _description = "Base Channel Connector Component" diff --git a/connector_pms/components/deleter.py b/connector_pms/components/deleter.py new file mode 100644 index 0000000000..3e9fb97703 --- /dev/null +++ b/connector_pms/components/deleter.py @@ -0,0 +1,9 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import AbstractComponent + + +class ChannelDeleter(AbstractComponent): + _name = "channel.deleter" + _inherit = ["base.deleter"] diff --git a/connector_pms/components/exporter.py b/connector_pms/components/exporter.py new file mode 100644 index 0000000000..328a17f1b7 --- /dev/null +++ b/connector_pms/components/exporter.py @@ -0,0 +1,69 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo.addons.component.core import AbstractComponent + +_logger = logging.getLogger(__name__) + + +class ChannelExporter(AbstractComponent): + """Base exporter for Channel""" + + _name = "channel.exporter" + _inherit = "generic.exporter.custom" + + _usage = "direct.record.exporter" + + +class ChannelBatchExporter(AbstractComponent): + """The role of a BatchExporter is to search for a list of + items to import, then it can either import them directly or delay + the import of each item separately. + """ + + _name = "channel.batch.exporter" + _inherit = "base.exporter" + + def run(self, domain=None): + """Run the batch synchronization""" + if not domain: + domain = [] + relation_model = self.binder_for().unwrap_model() + for relation in self.env[relation_model].search(domain): + self._export_record(relation) + + def _export_record(self, external_id): + """Export a record directly or delay the export of the record. + + Method to implement in sub-classes. + """ + raise NotImplementedError + + +class ChannelDirectBatchExporter(AbstractComponent): + """Import the records directly, without delaying the jobs.""" + + _name = "channel.direct.batch.exporter" + _inherit = "channel.batch.exporter" + + _usage = "direct.batch.exporter" + + def _export_record(self, relation): + """export the record directly""" + self.model.export_record(self.backend_record, relation) + + +class ChannelDelayedBatchExporter(AbstractComponent): + """Delay import of the records""" + + _name = "channel.delayed.batch.exporter" + _inherit = "channel.batch.exporter" + + _usage = "delayed.batch.exporter" + + def _export_record(self, relation, job_options=None): + """Delay the export of the records""" + delayable = self.model.with_delay(**job_options or {}) + delayable.export_record(self.backend_record, relation) diff --git a/connector_pms/components/importer.py b/connector_pms/components/importer.py new file mode 100644 index 0000000000..cfe0923507 --- /dev/null +++ b/connector_pms/components/importer.py @@ -0,0 +1,83 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo.addons.component.core import AbstractComponent + +_logger = logging.getLogger(__name__) + + +class ChannelImporter(AbstractComponent): + """Base importer for Channel""" + + _name = "channel.importer" + _inherit = "generic.importer.custom" + + _usage = "direct.record.importer" + + +class ChannelBatchImporter(AbstractComponent): + """The role of a BatchImporter is to search for a list of + items to import, then it can either import them directly or delay + the import of each item separately. + """ + + _name = "channel.batch.importer" + _inherit = "base.importer" + + # def run(self, domain=[]): + # """ Run the synchronization """ + # record_ids = self.backend_adapter.search(domain) + # for record_id in record_ids: + # self._import_record(record_id) + + def run(self, domain=None): + """Run the synchronization""" + if domain is None: + domain = [] + records = self.backend_adapter.search_read(domain) + for rec in records: + self._import_record(rec[self.backend_adapter._id], external_data=rec) + + def _import_record(self, external_id, external_data=None): + """Import a record directly or delay the import of the record. + + Method to implement in sub-classes. + """ + raise NotImplementedError + + +class ChannelDirectBatchImporter(AbstractComponent): + """Import the records directly, without delaying the jobs.""" + + _name = "channel.direct.batch.importer" + _inherit = "channel.batch.importer" + + _usage = "direct.batch.importer" + + def _import_record(self, external_id, external_data=None): + """Import the record directly""" + if external_data is None: + external_data = {} + self.model.import_record( + self.backend_record, external_id, external_data=external_data + ) + + +class ChannelDelayedBatchImporter(AbstractComponent): + """Delay import of the records""" + + _name = "channel.delayed.batch.importer" + _inherit = "channel.batch.importer" + + _usage = "delayed.batch.importer" + + def _import_record(self, external_id, external_data=None, job_options=None): + """Delay the import of the records""" + if external_data is None: + external_data = {} + delayable = self.model.with_delay(**job_options or {}) + delayable.import_record( + self.backend_record, external_id, external_data=external_data + ) diff --git a/connector_pms/components/mapper_export.py b/connector_pms/components/mapper_export.py new file mode 100644 index 0000000000..2f2c452d71 --- /dev/null +++ b/connector_pms/components/mapper_export.py @@ -0,0 +1,19 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import AbstractComponent + + +class ChannelMapperExport(AbstractComponent): + _name = "channel.mapper.export" + _inherit = "base.export.mapper" + + +class ChannelChildMapperExport(AbstractComponent): + _name = "channel.child.mapper.export" + _inherit = "base.map.child.export" + + +class ChannelChildBinderMapperExport(AbstractComponent): + _name = "channel.child.binder.mapper.export" + _inherit = "base.map.child.binder.export" diff --git a/connector_pms/components/mapper_import.py b/connector_pms/components/mapper_import.py new file mode 100644 index 0000000000..cbbb74fb31 --- /dev/null +++ b/connector_pms/components/mapper_import.py @@ -0,0 +1,19 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import AbstractComponent + + +class ChannelMapperImport(AbstractComponent): + _name = "channel.mapper.import" + _inherit = "base.import.mapper" + + +class ChannelChildMapperImport(AbstractComponent): + _name = "channel.child.mapper.import" + _inherit = "base.map.child.import" + + +class ChannelChildBinderMapperImport(AbstractComponent): + _name = "channel.child.binder.mapper.import" + _inherit = "base.map.child.binder.import" diff --git a/connector_pms/components_custom/__init__.py b/connector_pms/components_custom/__init__.py new file mode 100644 index 0000000000..172324a818 --- /dev/null +++ b/connector_pms/components_custom/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import binder +from . import importer +from . import exporter +from . import mapper diff --git a/connector_pms/components_custom/binder.py b/connector_pms/components_custom/binder.py new file mode 100644 index 0000000000..e5de098e14 --- /dev/null +++ b/connector_pms/components_custom/binder.py @@ -0,0 +1,267 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import _, fields, models, tools +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import AbstractComponent +from odoo.addons.connector.exception import InvalidDataError + +_logger = logging.getLogger(__name__) + + +class BinderCustom(AbstractComponent): + _name = "base.binder.custom" + _inherit = "base.binder" + + _sync_date_export_field = "sync_date_export" + + _internal_alt_id_field = "_internal_alt_id" + _external_alt_id_field = "_external_alt_id" + + def bind(self, external_id, binding, export=False): + """Create the link between an external ID and an Odoo ID + + :param external_id: external id to bind + :param binding: Odoo record to bind + :type binding: int + """ + # Prevent False, None, or "", but not 0 + assert ( + external_id or external_id == 0 + ) and binding, "external_id or binding missing, " "got: %s, %s" % ( + external_id, + binding, + ) + # avoid to trigger the export when we modify the `external_id` + now_fmt = fields.Datetime.now() + if isinstance(binding, models.BaseModel): + binding.ensure_one() + else: + binding = self.model.browse(binding) + binding.with_context(connector_no_export=True).write( + { + self._external_field: tools.ustr(external_id), + export + and self._sync_date_export_field + or self._sync_date_field: now_fmt, + } + ) + + def wrap_record(self, relation, force=False): + """Give the real record + + :param relation: Odoo real record for which we want to get its binding + :param force: if this is True and not binding found it creates an + empty binding + :return: binding corresponding to the real record or + empty recordset if the record has no binding + """ + if isinstance(relation, models.BaseModel): + relation.ensure_one() + else: + if not isinstance(relation, int): + raise InvalidDataError( + "The real record (relation) must be a " + "regular Odoo record or an id (integer)" + ) + relation = self.model.browse(relation) + if not relation: + raise InvalidDataError("The real record (relation) does not exist") + + if self.model._name == relation._name: + raise Exception( + _( + "The object '%s' is already wrapped, it's already a binding object. " + "You can only wrap Odoo objects" + ) + % (relation) + ) + + binding = self.model.with_context(active_test=False).search( + [ + (self._odoo_field, "=", relation.id), + (self._backend_field, "=", self.backend_record.id), + ] + ) + + if not binding: + if force: + binding = self.model.with_context(connector_no_export=True).create( + { + self._odoo_field: relation.id, + self._backend_field: self.backend_record.id, + } + ) + else: + binding = self.model + + if len(binding) > 1: + raise InvalidDataError("More than one binding found") + + return binding + + def _check_domain(self, domain): + for field, _r, value in domain: + if isinstance(value, (list, tuple)): + for e in value: + if isinstance(e, (tuple, list, set, dict)): + raise ValidationError( + _( + "Wrong domain value type '%(type)s' " + "on value '%(value)s' of field '%(field)s'" + ) + % {"type": type(e), "value": e, "field": field} + ) + + def _get_internal_record_domain(self, values): + return [(k, "=", v) for k, v in values.items()] + + def _get_internal_record_alt(self, model_name, values): + domain = self._get_internal_record_domain(values) + self._check_domain(domain) + return self.env[model_name].search(domain) + + def to_binding_from_external_key(self, mapper): + """ + :param mapper: + :return: binding with alternate external key + """ + internal_alt_id = getattr(self, self._internal_alt_id_field, None) + if internal_alt_id: + if isinstance(internal_alt_id, str): + internal_alt_id = [internal_alt_id] + all_values = mapper.values(for_create=True) + if any([x not in all_values for x in internal_alt_id]): + raise InvalidDataError( + "The alternative id (_internal_alt_id) '%s' must exist on mapper" + % internal_alt_id + ) + model_name = self.unwrap_model() + id_values = {x: all_values[x] for x in internal_alt_id} + record = self._get_internal_record_alt(model_name, id_values) + if len(record) > 1: + raise InvalidDataError( + "More than one internal records found. " + "The alternate internal id field '%s' is not unique" + % (internal_alt_id,) + ) + if record: + binding = self.wrap_record(record) + if not binding: + values = { + k: all_values[k] + for k in set(self.model._model_fields) & set(all_values) + } + if self._odoo_field in values: + if values[self._odoo_field] != record.id: + raise InvalidDataError( + "The id found on the mapper ('%i') " + "is not the one expected ('%i')" + % (values[self._odoo_field], record.id) + ) + else: + values[self._odoo_field] = record.id + binding = self.model.create(values) + _logger.debug("%d linked from Backend", binding) + return binding + + return self.model + + def _get_external_record_domain(self, values): + return [(k, "=", v) for k, v in values.items()] + + def _get_external_record_alt(self, values): + domain = self._get_external_record_domain(values) + adapter = self.component(usage="backend.adapter") + return adapter.search_read(domain) + + def to_binding_from_internal_key(self, relation): + """ + Given an odoo object (not binding object) without binding related + :param relation: odoo object, not a binding and without binding + :return: binding + """ + ext_alt_id = getattr(self, self._external_alt_id_field, None) + if not ext_alt_id: + return self.model + + if isinstance(ext_alt_id, str): + ext_alt_id = [ext_alt_id] + int_alt_id = getattr(self, self._internal_alt_id_field, None) + if not int_alt_id: + raise InvalidDataError( + "The alternative id (_external_alt_id) is not defined on binder" + ) + if isinstance(int_alt_id, str): + int_alt_id = [int_alt_id] + + export_mapper = self.component(usage="export.mapper") + mapper_external_data = export_mapper.map_record(relation) + id_fields = mapper_external_data._mapper.get_target_fields( + mapper_external_data, fields=ext_alt_id + ) + if not id_fields: + raise ValidationError( + _("External alternative id '%s' not found in export mapper") + % (ext_alt_id,) + ) + id_values = mapper_external_data.values(for_create=True, fields=id_fields) + record = self._get_external_record_alt(id_values) + if record: + if len(record) > 1: + raise InvalidDataError( + "More than one external records found. " + "The alternate external id field '%s' is not " + "unique in the backend" % (ext_alt_id,) + ) + record = record[0] + + adapter = self.component(usage="backend.adapter") + external_id = record[adapter._id] + + binding = self.wrap_record(relation) + if binding: + current_external_id = self.to_external(binding) + if not current_external_id: + self.bind(external_id, binding, export=True) + else: + if current_external_id != external_id: + raise InvalidDataError( + "Integrity error: The current external_id '%s' " + "should be the same as the one we are trying " + "to assign '%s'" % (current_external_id, external_id) + ) + _logger.debug("%d already binded to Backend", binding) + # return binding + else: + import_mapper = self.component(usage="import.mapper") + mapper_internal_data = import_mapper.map_record(record) + + binding_ext_fields = mapper_internal_data._mapper.get_target_fields( + mapper_internal_data, fields=self.model._model_fields + ) + importer = self.component(usage="direct.record.importer") + importer.run( + external_id, + external_data=record, + external_fields=binding_ext_fields, + ) + binding = self.to_internal(external_id) + + if not binding: + raise InvalidDataError( + "The binding with external id '%s' " + "not found and it should be" % external_id + ) + _logger.debug("%d linked to Backend", binding) + return binding + + return self.model + + +# TODO: naming the methods more intuitively +# TODO: unify both methods, they have a lot of common code +# TODO: extract parts to smaller and common methods reused by the main methods +# TODO: use .new instead of dicts on to_binding_from_internal_key diff --git a/connector_pms/components_custom/exporter.py b/connector_pms/components_custom/exporter.py new file mode 100644 index 0000000000..96f7eccb9d --- /dev/null +++ b/connector_pms/components_custom/exporter.py @@ -0,0 +1,324 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +from contextlib import contextmanager + +import psycopg2 + +from odoo import _ + +from odoo.addons.component.core import AbstractComponent +from odoo.addons.connector.exception import IDMissingInBackend, RetryableJobError + +_logger = logging.getLogger(__name__) + + +class GenericExporterCustom(AbstractComponent): + """Generic Synchronizer for exporting data from Odoo to a backend""" + + _name = "generic.exporter.custom" + _inherit = "base.exporter" + + _default_binding_field = None + + def __init__(self, working_context): + super(GenericExporterCustom, self).__init__(working_context) + self.binding = None + self.external_id = None + + def _should_import(self): + return False + + def _delay_import(self): + """Schedule an import of the record. + + Adapt in the sub-classes when the model is not imported + using ``import_record``. + """ + # force is True because the sync_date will be more recent + # so the import would be skipped + assert self.external_id + self.binding.with_delay().import_record( + self.backend_record, self.external_id, force=True + ) + + def _mapper_options(self): + return {"binding": self.binding} + + def _force_binding_creation(self, relation): + if not self.binding: + self.binding = self.binder.wrap_record(relation, force=True) + + def run(self, relation, *args, **kwargs): + """Run the synchronization + + :param binding: binding record to export + """ + # get binding from real record + self.binding = self.binder.wrap_record(relation) + + # if not binding, try to link to existing external record with + # the same alternate key and create/update binding + if not self.binding or not self.binding.external_id: + self.binding = ( + self.binder.to_binding_from_internal_key(relation) or self.binding + ) + + # if still not binding, create an empty one + self._force_binding_creation(relation) + + if self.binding: + self.external_id = self.binder.to_external(self.binding) + + try: + should_import = self._should_import() + except IDMissingInBackend: + self.external_id = None + should_import = False + if should_import: + self._delay_import() + + result = self._run(*args, **kwargs) + + self.binder.bind(self.external_id, self.binding, export=True) + # Commit so we keep the external ID when there are several + # exports (due to dependencies) and one of them fails. + # The commit will also release the lock acquired on the binding + # record + # if not odoo.tools.config["test_enable"]: + # self.env.cr.commit() # pylint: disable=E8102 + + self._after_export() + return result + + def _run(self, internal_fields=None): + """Flow of the synchronization, implemented in inherited classes""" + assert self.binding + + if not self.external_id: + internal_fields = None # should be created with all the fields + + if self._has_to_skip(): + return _("Nothing to export") + + # export the missing linked resources + self._export_dependencies() + + # prevent other jobs to export the same record + # will be released on commit (or rollback) + self._lock() + + map_record = self._map_data() + + # passing info to the mapper + opts = self._mapper_options() + + if self.external_id: + values = self._update_data(map_record, fields=internal_fields, **opts) + if not values: + return _("Nothing to export") + self._update(values) + else: + values = self._create_data(map_record, fields=internal_fields, **opts) + if not values: + return _("Nothing to export") + self.external_id = self._create(values) + + return _("Record exported with ID %s on Backend.") % self.external_id + + def _after_export(self): + """Can do several actions after exporting a record on the backend""" + + def _lock(self): + """Lock the binding record. + + Lock the binding record so we are sure that only one export + job is running for this record if concurrent jobs have to export the + same record. + + When concurrent jobs try to export the same record, the first one + will lock and proceed, the others will fail to lock and will be + retried later. + + This behavior works also when the export becomes multilevel + with :meth:`_export_dependencies`. Each level will set its own lock + on the binding record it has to export. + + """ + sql = "SELECT id FROM %s WHERE ID = %%s FOR UPDATE NOWAIT" % self.model._table + try: + self.env.cr.execute(sql, (self.binding.id,), log_exceptions=False) + except psycopg2.OperationalError as err: + _logger.info( + "A concurrent job is already exporting the same " + "record (%s with id %s). Job delayed later.", + self.model._name, + self.binding.id, + ) + raise RetryableJobError( + "A concurrent job is already exporting the same record " + "(%s with id %s). The job will be retried later." + % (self.model._name, self.binding.id) + ) from err + + def _has_to_skip(self): + """Return True if the export can be skipped""" + return False + + @contextmanager + def _retry_unique_violation(self): + """Context manager: catch Unique constraint error and retry the + job later. + + When we execute several jobs workers concurrently, it happens + that 2 jobs are creating the same record at the same time (binding + record created by :meth:`_export_dependency`), resulting in: + + IntegrityError: duplicate key value violates unique + constraint "my_backend_product_product_odoo_uniq" + DETAIL: Key (backend_id, odoo_id)=(1, 4851) already exists. + + In that case, we'll retry the import just later. + + .. warning:: The unique constraint must be created on the + for the same External record. + + """ + try: + yield + except psycopg2.IntegrityError as err: + if err.pgcode == psycopg2.errorcodes.UNIQUE_VIOLATION: + raise RetryableJobError( + "A database error caused the failure of the job:\n" + "%s\n\n" + "Likely due to 2 concurrent jobs wanting to create " + "the same record. The job will be retried later." % err + ) from err + else: + raise + + def _export_dependency( + self, + relation, + binding_model, + component_usage="direct.record.exporter", + binding_field=None, + binding_extra_vals=None, + always=False, + ): + """ + Export a dependency. The exporter class is a subclass of + ``GenericExporter``. If a more precise class need to be defined, + it can be passed to the ``exporter_class`` keyword argument. + + .. warning:: a commit is done at the end of the export of each + dependency. The reason for that is that we pushed a record + on the backend and we absolutely have to keep its ID. + + So you *must* take care not to modify the Odoo + database during an export, excepted when writing + back the external ID or eventually to store + external data that we have to keep on this side. + + You should call this method only at the beginning + of the exporter synchronization, + in :meth:`~._export_dependencies`. + + :param relation: record to export if not already exported + :type relation: :py:class:`odoo.models.BaseModel` + :param binding_model: name of the binding model for the relation + :type binding_model: str | unicode + :param component_usage: 'usage' to look for to find the Component to + for the export, by default 'record.exporter' + :type exporter: str | unicode + :param binding_field: name of the one2many field on a normal + record that points to the binding record + (default: my_backend_bind_ids). + It is used only when the relation is not + a binding but is a normal record. + :type binding_field: str | unicode + :binding_extra_vals: In case we want to create a new binding + pass extra values for this binding + :type binding_extra_vals: dict + """ + if not relation: + return + + binding = None + if not always: + rel_binder = self.binder_for(binding_model) + binding = rel_binder.wrap_record(relation) + if not binding or not binding.external_id: + binding = rel_binder.to_binding_from_internal_key(relation) + + if always or not binding: + exporter = self.component(usage=component_usage, model_name=binding_model) + exporter.run(relation) + + def _export_dependencies(self): + """Export the dependencies for the record""" + return + + def _map_data(self): + """Returns an instance of + :py:class:`~odoo.addons.connector.components.mapper.MapRecord` + + """ + return self.mapper.map_record(self.binding) + + def _validate_create_data(self, data): + """Check if the values to import are correct + + Pro-actively check before the ``Model.create`` if some fields + are missing or invalid + + Raise `InvalidDataError` + """ + return + + def _validate_update_data(self, data): + """Check if the values to import are correct + + Pro-actively check before the ``Model.update`` if some fields + are missing or invalid + + Raise `InvalidDataError` + """ + return + + def _create_data(self, map_record, fields=None, **kwargs): + """Get the data to pass to :py:meth:`_create`""" + return map_record.values(for_create=True, fields=fields, **kwargs) + + def _create(self, data): + """Create the External record""" + # special check on data before export + self._validate_create_data(data) + # if self.backend_record.export_disabled: + # _logger.info( + # _( + # "The backend record creation is not allowed. Export is disabled, data: %s" + # ) + # % data + # ) + # return None + return self.backend_adapter.create(data) + + def _update_data(self, map_record, fields=None, **kwargs): + """Get the data to pass to :py:meth:`_update`""" + return map_record.values(fields=fields, **kwargs) + + def _update(self, data): + """Update an External record""" + assert self.external_id + # special check on data before export + self._validate_update_data(data) + # if self.backend_record.export_disabled: + # _logger.info( + # _( + # "The backend record update is not allowed. Export is disabled, data: %s" + # ) + # % data + # ) + # return + self.backend_adapter.write(self.external_id, data) diff --git a/connector_pms/components_custom/importer.py b/connector_pms/components_custom/importer.py new file mode 100644 index 0000000000..7e87fff5d9 --- /dev/null +++ b/connector_pms/components_custom/importer.py @@ -0,0 +1,238 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +from contextlib import contextmanager + +import psycopg2 + +from odoo import _ +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import AbstractComponent +from odoo.addons.connector.exception import IDMissingInBackend +from odoo.addons.queue_job.exception import NothingToDoJob, RetryableJobError + +_logger = logging.getLogger(__name__) + + +class GenericImporterCustom(AbstractComponent): + """Generic Synchronizer for importing data from backend to Odoo""" + + _name = "generic.importer.custom" + _inherit = "base.importer" + + @contextmanager + def _retry_unique_violation(self): + """Context manager: catch Unique constraint error and retry the + job later. + + When we execute several jobs workers concurrently, it happens + that 2 jobs are creating the same record at the same time (binding + record created by :meth:`_export_dependency`), resulting in: + + IntegrityError: duplicate key value violates unique + constraint "prestashop_product_template_openerp_uniq" + DETAIL: Key (backend_id, odoo_id)=(1, 4851) already exists. + + In that case, we'll retry the import just later. + + """ + try: + yield + except psycopg2.IntegrityError as err: + if err.pgcode == psycopg2.errorcodes.UNIQUE_VIOLATION: + raise RetryableJobError( + "A database error caused the failure of the job:\n" + "%s\n\n" + "Likely due to 2 concurrent jobs wanting to create " + "the same record. The job will be retried later." % err + ) from err + else: + raise + + def _import_dependency( + self, external_ids, binding_model, importer=None, adapter=None, always=False + ): + """Import a dependency. + + The importer class is a class or subclass of + :class:`Importer`. A specific class can be defined. + + :param external_ids: id or id's of the related bindings to import + :param binding_model: name of the binding model for the relation + :type binding_model: str | unicode + :param importer_component: component to use for import + By default: 'importer' + :type importer_component: Component + :param adapter_component: component to use for access to backend + By default: 'backend.adapter' + :type adapter_component: Component + :param always: if True, the record is updated even if it already + exists, note that it is still skipped if it has + not been modified on Backend since the last + update. When False, it will import it only when + it does not yet exist. + :type always: boolean + """ + if not external_ids: + return + if not isinstance(external_ids, (list, tuple, set)): + external_ids = [external_ids] + + if importer is None: + importer = self.component(usage=self._usage, model_name=binding_model) + + binder = self.binder_for(binding_model) + external_ids = list( + filter(lambda x: always or not binder.to_internal(x), external_ids) + ) + if len(external_ids) == 1: + try: + importer.run(external_ids[0]) + except NothingToDoJob: + _logger.info( + "Dependency import of %s(%s) has been ignored.", + binding_model._name, + external_ids[0], + ) + elif len(external_ids) > 1: + if adapter is None: + adapter = self.component( + usage="backend.adapter", model_name=binding_model + ) + if not hasattr(adapter, "_id"): + raise ValidationError(_("No Id field (_id) defined on backend")) + records = adapter.search_read([(adapter._id, "in", external_ids)]) + for rec in records: + external_id = rec[adapter._id] + try: + importer.run(external_id, external_data=rec) + except NothingToDoJob: + _logger.info( + "Dependency import of %s(%s) has been ignored.", + binding_model._name, + external_ids, + ) + + def _import_dependencies(self, external_data, external_fields): + """Import the dependencies for the record + + Import of dependencies can be done manually or by calling + :meth:`_import_dependency` for each dependency. + """ + return + + def _after_import(self, binding): + return + + def _must_skip(self, binding): + """Hook called right after we read the data from the backend. + + If the method returns a message giving a reason for the + skipping, the import will be interrupted and the message + recorded in the job (if the import is called directly by the + job, not by dependencies). + + If it returns None, the import will continue normally. + + :returns: None | str | unicode + """ + return False + + def _mapper_options(self, binding): + return {"binding": binding} + + def _create(self, model, values): + """Create the Internal record""" + # return model.create(values) + return model.with_context( + connector_no_export=True, + force_write_blocked=True, + ).create(values) + + def _update(self, binding, values): + """Update an Internal record""" + binding.with_context( + connector_no_export=True, + force_write_blocked=True, + ).write(values) + + def run(self, external_id, external_data=None, external_fields=None): + if not external_data: + external_data = {} + lock_name = "import({}, {}, {}, {})".format( + self.backend_record._name, + self.backend_record.id, + self.work.model_name, + external_id, + ) + # Keep a lock on this import until the transaction is committed + # The lock is kept since we have detected that the informations + # will be updated into Odoo + self.advisory_lock_or_retry(lock_name, retry_seconds=10) + + if not external_data: + # read external data from Backend + external_data = self.backend_adapter.read(external_id) + if not external_data: + raise IDMissingInBackend( + _("Record with external_id '%s' does not exist in Backend") + % (external_id,) + ) + + # REVIEW: Avoid import modified folios + if external_data.get("was_modified"): + return True + + # import the missing linked resources + self._import_dependencies(external_data, external_fields) + + # map_data + # this one knows how to convert backend data to odoo data + mapper = self.component(usage="import.mapper") + + # convert to odoo data + internal_data = mapper.map_record(external_data) + + # get_binding + # this one knows how to link Baclend/Odoo records + binder = self.component(usage="binder") + + # find if the external id already exists in odoo + binding = binder.to_internal(external_id) + + # if binding not exists, try to link existing internal object + if not binding: + binding = binder.to_binding_from_external_key(internal_data) + + # skip binding + skip = self._must_skip(binding) + if skip: + return skip + + # passing info to the mapper + opts = self._mapper_options(binding) + + if external_fields != []: + # persist data + if binding: + # if exists, we update it + values = internal_data.values(fields=external_fields, **opts) + self._update(binding, values) + _logger.debug("%d updated from Backend %s", binding, external_id) + else: + # or we create it + values = internal_data.values( + for_create=True, fields=external_fields, **opts + ) + with self._retry_unique_violation(): + binding = self._create(self.model, values) + _logger.debug("%d created from Backend %s", binding, external_id) + + # finally, we bind both, so the next time we import + # the record, we'll update the same record instead of + # creating a new one + binder.bind(external_id, binding) + + # last update + self._after_import(binding) diff --git a/connector_pms/components_custom/mapper.py b/connector_pms/components_custom/mapper.py new file mode 100644 index 0000000000..64e7d1081d --- /dev/null +++ b/connector_pms/components_custom/mapper.py @@ -0,0 +1,176 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import collections.abc +import logging +import uuid + +from odoo import _, fields +from odoo.exceptions import ValidationError + +from odoo.addons.component.core import AbstractComponent +from odoo.addons.connector.components.mapper import m2o_to_external + +_logger = logging.getLogger(__name__) + + +class Mapper(AbstractComponent): + _inherit = "base.mapper" + + def _apply_with_options(self, map_record): + """ + Hack to allow having non required children field + """ + assert ( + self.options is not None + ), "options should be defined with '_mapping_options'" + _logger.debug("converting record %s to model %s", map_record.source, self.model) + + fields = self.options.fields + for_create = self.options.for_create + result = {} + for from_attr, to_attr in self.direct: + if isinstance(from_attr, collections.abc.Callable): + attr_name = self._direct_source_field_name(from_attr) + else: + attr_name = from_attr + + if not fields or attr_name in fields: + value = self._map_direct(map_record.source, from_attr, to_attr) + result[to_attr] = value + + for meth, definition in self.map_methods: + mapping_changed_by = definition.changed_by + if not fields or ( + mapping_changed_by and mapping_changed_by.intersection(fields) + ): + if definition.only_create and not for_create: + continue + values = meth(map_record.source) + if not values: + continue + if not isinstance(values, dict): + raise ValueError( + "%s: invalid return value for the " + "mapping method %s" % (values, meth) + ) + result.update(values) + + for from_attr, to_attr, model_name in self.children: + if not fields or from_attr in fields: + if from_attr in map_record.source: + items = self._map_child(map_record, from_attr, to_attr, model_name) + if items: + result[to_attr] = items + return self.finalize(map_record, result) + + def get_target_fields(self, map_record, fields): + if not fields: + return [] + fields = set(fields) + result = {} + for from_attr, to_attr in self.direct: + if isinstance(from_attr, collections.abc.Callable): + # attr_name = self._direct_source_field_name(from_attr) + # TODO + raise NotImplementedError + else: + if to_attr in fields: + if to_attr in result: + raise ValidationError(_("Field '%s' mapping defined twice")) + result[to_attr] = from_attr + + # TODO: create a new decorator to write the field mapping manually + # I think this is not necessary, just use changed_by is precisely for that + # for meth, definition in self.map_methods: + # for mcb in definition.mapping: + # if mcb in fields: + # if to_attr in result: + # raise ValidationError("Field '%s' mapping defined twice") + # result[to_attr] = from_attr + + for from_attr, to_attr, _model_name in self.children: + if to_attr in fields: + if to_attr in result: + raise ValidationError(_("Field '%s' mapping defined twice")) + result[to_attr] = from_attr + + return list(set(result.values())) + + +class ChannelChildMapperImport(AbstractComponent): + _inherit = "base.map.child" + + def get_all_items(self, mapper, items, parent, to_attr, options): + mapped = [] + for item in items: + map_record = mapper.map_record(item, parent=parent) + if self.skip_item(map_record): + continue + item_values = self.get_item_values(map_record, to_attr, options) + if item_values: + self._child_bind(map_record, item_values) + mapped.append(item_values) + return mapped + + def get_items(self, items, parent, to_attr, options): + mapper = self._child_mapper() + mapped = self.get_all_items(mapper, items, parent, to_attr, options) + return self.format_items(mapped) + + def _child_bind(self, map_record, item_values): + return + + +class ImportMapChildBinder(AbstractComponent): + _name = "base.map.child.binder.import" + _inherit = "base.map.child.import" + + def _child_bind(self, map_record, item_values): + binder = self.binder_for() + if binder._external_field not in item_values: + item_values[binder._external_field] = uuid.uuid4().hex + item_values[binder._sync_date_field] = fields.Datetime.now() + + +class ExportMapChildBinder(AbstractComponent): + _name = "base.map.child.binder.export" + _inherit = "base.map.child.export" + + def _child_bind(self, map_record, item_values): + binder = self.binder_for() + external_id = map_record.source.external_id or uuid.uuid4().hex + binder.bind(external_id, map_record.source, export=True) + + +# TODO: create a fix on OCA repo and remove this class +class ExportMapper(AbstractComponent): + _inherit = "base.export.mapper" + + def _map_direct(self, record, from_attr, to_attr): + """Apply the ``direct`` mappings. + + :param record: record to convert from a source to a target + :param from_attr: name of the source attribute or a callable + :type from_attr: callable | str + :param to_attr: name of the target attribute + :type to_attr: str + """ + if isinstance(from_attr, collections.abc.Callable): + return from_attr(self, record, to_attr) + + value = record[from_attr] + if value is None: # we need to allow fields with value 0 + return False + + # Backward compatibility: when a field is a relation, and a modifier is + # not used, we assume that the relation model is a binding. + # Use an explicit modifier m2o_to_external in the 'direct' mappings to + # change that. + field = self.model._fields[from_attr] + if field.type == "many2one": + mapping_func = m2o_to_external(from_attr) + value = mapping_func(self, record, to_attr) + return value + + +# TODO: move uuid to generic binder diff --git a/connector_pms/data/queue_data.xml b/connector_pms/data/queue_data.xml new file mode 100644 index 0000000000..0ba68ae75d --- /dev/null +++ b/connector_pms/data/queue_data.xml @@ -0,0 +1,9 @@ + + + + + pms + + + diff --git a/connector_pms/models/__init__.py b/connector_pms/models/__init__.py new file mode 100644 index 0000000000..361a081a8d --- /dev/null +++ b/connector_pms/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import common +from . import pms_reservation +from . import queue_job diff --git a/connector_pms/models/common/__init__.py b/connector_pms/models/common/__init__.py new file mode 100644 index 0000000000..d7f2e63747 --- /dev/null +++ b/connector_pms/models/common/__init__.py @@ -0,0 +1,8 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import backend +from . import backend_type +from . import backend_log +from . import backend_method +from . import binding diff --git a/connector_pms/models/common/backend.py b/connector_pms/models/common/backend.py new file mode 100644 index 0000000000..3b2b0508c2 --- /dev/null +++ b/connector_pms/models/common/backend.py @@ -0,0 +1,68 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class ChannelBackend(models.Model): + _name = "channel.backend" + _description = "Channel PMS Backend" + + name = fields.Char(required=True) + + pms_property_id = fields.Many2one( + comodel_name="pms.property", + string="Property", + required=True, + ondelete="restrict", + ) + + user_id = fields.Many2one( + comodel_name="res.users", + string="User", + ondelete="restrict", + ) + + backend_type_id = fields.Many2one( + string="Type", + comodel_name="channel.backend.type", + required=True, + ondelete="restrict", + ) + + export_disabled = fields.Boolean() + + @property + def child_id(self): + self.ensure_one() + # TODO: move to computed field + model = self.env[self.backend_type_id.model_type_id.model]._main_model + child_backends = self.env[model].search( + [ + ("parent_id", "=", self.id), + ] + ) + if len(child_backends) > 1: + raise ValidationError( + _( + "Inconsistency detected. More than one " + "backend's child found for the same parent" + ) + ) + return child_backends + + def channel_config(self): + self.ensure_one() + # TODO: move to computed field + model = self.env[self.backend_type_id.model_type_id.model]._main_model + return { + "type": "ir.actions.act_window", + "res_model": model, + "views": [[False, "form"]], + "context": not self.child_id and {"default_parent_id": self.id}, + "res_id": self.child_id.id, + } diff --git a/connector_pms/models/common/backend_log.py b/connector_pms/models/common/backend_log.py new file mode 100644 index 0000000000..8e4c2facca --- /dev/null +++ b/connector_pms/models/common/backend_log.py @@ -0,0 +1,47 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class ChannelBackendLog(models.Model): + _name = "channel.backend.log" + _order = "timestamp desc" + _description = "Channel PMS Backend API Log" + + backend_id = fields.Many2one( + comodel_name="channel.backend", + string="Backend", + required=True, + readonly=True, + ondelete="restrict", + ) + + timestamp = fields.Datetime( + required=True, + readonly=True, + ) + + method_id = fields.Many2one( + comodel_name="channel.backend.method", + string="Method", + required=True, + readonly=True, + ondelete="restrict", + ) + + arguments = fields.Text( + required=True, + readonly=True, + ) + response_code = fields.Text( + required=True, + readonly=True, + ) + response = fields.Text( + required=True, + readonly=True, + ) diff --git a/connector_pms/models/common/backend_method.py b/connector_pms/models/common/backend_method.py new file mode 100644 index 0000000000..aa11de77f7 --- /dev/null +++ b/connector_pms/models/common/backend_method.py @@ -0,0 +1,40 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class ChannelBackendMethod(models.Model): + _name = "channel.backend.method" + _description = "Channel PMS Backend Method" + + name = fields.Char( + required=True, + ) + + backend_type_id = fields.Many2one( + comodel_name="channel.backend.type", + string="Backend Type", + required=True, + ondelete="restrict", + ) + + max_calls = fields.Integer( + help="Maximum number of calls to this method in the defined time window", + ) + + time_window = fields.Integer( + string="Time Window (seconds)", + help="Time window in seconds", + ) + + _sql_constraints = [ + ( + "method_backend_type", + "unique(name, backend_type_id)", + "Only one method with the same name is allowed per Backend Type", + ), + ] diff --git a/connector_pms/models/common/backend_type.py b/connector_pms/models/common/backend_type.py new file mode 100644 index 0000000000..0415d3a41b --- /dev/null +++ b/connector_pms/models/common/backend_type.py @@ -0,0 +1,57 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class ChannelBackendType(models.Model): + _name = "channel.backend.type" + _description = "Channel PMS Backend Type" + + name = fields.Char(required=True) + + model_type_id = fields.Many2one( + comodel_name="ir.model", + string="Referenced Model Type", + required=True, + ondelete="cascade", + domain=lambda self: [ + ("model", "in", self._get_channel_backend_type_model_names()) + ], + ) + + @property + def child_id(self): + self.ensure_one() + child_backends = self.env[self.model_type_id.model].search( + [ + ("parent_id", "=", self.id), + ] + ) + if len(child_backends) > 1: + raise ValidationError( + _( + "Inconsistency detected. More than one " + "backend's child found for the same parent" + ) + ) + return child_backends + + @api.model + def _get_channel_backend_type_model_names(self): + res = [] + return res + + def channel_type_config(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "res_model": self.model_type_id.model, + "views": [[False, "form"]], + "context": not self.child_id and {"default_parent_id": self.id}, + "res_id": self.child_id.id, + } diff --git a/connector_pms/models/common/binding.py b/connector_pms/models/common/binding.py new file mode 100644 index 0000000000..8300f2e754 --- /dev/null +++ b/connector_pms/models/common/binding.py @@ -0,0 +1,138 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + +# from odoo.addons.queue_job.job import job, related_action + + +class ChannelBinding(models.AbstractModel): + _name = "channel.binding" + _inherit = "external.binding" + _description = "Channel PMS Binding (abstract)" + + external_id = fields.Integer(string="External ID", required=False) + + # by default we consider sync_date as the import one + sync_date = fields.Datetime(readonly=True, string="Last Sync (import)") + sync_date_export = fields.Datetime(readonly=True, string="Last Sync (export)") + + # TODO: move next 2 fields to external.binding in order to + # add this functionality to connector + actual_write_date = fields.Datetime( + string="Last Updated on (actual)", + help="Max write date between binding and actual record", + default=fields.Datetime.now(), + readonly=True, + store=True, + ) + + # We don't need synced attribute for import since we'd need to know the + # remote modification date not the local one + synced_export = fields.Boolean( + string="Synced (export)", compute="_compute_synced_export", readonly=True + ) + + def _is_synced_export(self): + self.ensure_one() + if not self.actual_write_date: + return True + return self.sync_date_export and self.sync_date_export >= self.actual_write_date + + @api.depends("sync_date_export", "actual_write_date") + def _compute_synced_export(self): + for rec in self: + rec.synced_export = rec._is_synced_export() + + _sql_constraints = [ + ( + "channel_external_uniq", + "unique(backend_id, external_id)", + "A binding already exists with the same External (Channel) ID.", + ), + ( + "channel_internal_uniq", + "unique(backend_id, odoo_id)", + "A binding already exists with the same Internal (Odoo) ID.", + ), + ] + + # default methods + @api.model + def import_data(self, backend_record=None): + """Prepare the batch import of records from Channel""" + return self.import_batch(backend_record=backend_record) + + @api.model + def export_data(self, backend_record=None): + """Prepare the batch export records to Channel""" + return self.export_batch(backend_record=backend_record) + + # syncronizer import + @api.model + def import_batch(self, backend_record, domain=None, delayed=True): + """Prepare the batch import of records modified on Channel""" + if not domain: + domain = [] + with backend_record.work_on(self._name) as work: + importer = work.component( + usage=delayed and "delayed.batch.importer" or "direct.batch.importer" + ) + return importer.run(domain=domain) + + @api.model + def import_record(self, backend_record, external_id, external_data=None): + """Import Channel record""" + if not external_data: + external_data = {} + with backend_record.work_on(self._name) as work: + importer = work.component(usage="direct.record.importer") + return importer.run(external_id, external_data=external_data) + + # syncronizer export + @api.model + def export_batch(self, backend_record, domain=None, delayed=True): + """Prepare the batch export of records modified on Odoo""" + if not domain: + domain = [] + with backend_record.work_on(self._name) as work: + exporter = work.component( + usage=delayed and "delayed.batch.exporter" or "direct.batch.exporter" + ) + return exporter.run(domain=domain) + + @api.model + def export_record(self, backend_record, relation): + """Export Odoo record""" + with backend_record.work_on(self._name) as work: + exporter = work.component(usage="direct.record.exporter") + return exporter.run(relation) + + # existing binding synchronization + def resync_import(self): + for record in self: + with record.backend_id.work_on(record._name) as work: + binder = work.component(usage="binder") + external_id = binder.to_external(record) + + func = record.import_record + if record.env.context.get("connector_delay"): + func = func.with_delay + + func(record.backend_id, external_id) + + return True + + def resync_export(self): + for record in self: + with record.backend_id.work_on(record._name) as work: + binder = work.component(usage="binder") + relation = binder.unwrap_binding(record) + + func = record.export_record + if record.env.context.get("connector_delay"): + func = func.with_delay + + func(record.backend_id, relation) + + return True diff --git a/connector_pms/models/pms_reservation.py b/connector_pms/models/pms_reservation.py new file mode 100644 index 0000000000..31b980f588 --- /dev/null +++ b/connector_pms/models/pms_reservation.py @@ -0,0 +1,28 @@ +# Copyright 2017-2018 Alexandre Díaz +# Copyright 2017 Dario Lodeiros +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class PmsReservation(models.Model): + _inherit = "pms.reservation" + + ota_reservation_code = fields.Char( + string="OTA Reservation Code", + readonly=True, + ) + + # pylint: disable=W8110 + @api.depends("ota_reservation_code") + def _compute_external_reference(self): + super(PmsReservation, self)._compute_external_reference() + + def _get_reservation_external_reference(self): + reference = super(PmsReservation, self)._get_reservation_external_reference() + if self.ota_reservation_code: + reference = self.ota_reservation_code + return reference diff --git a/connector_pms/models/queue_job.py b/connector_pms/models/queue_job.py new file mode 100644 index 0000000000..5209cfb4a5 --- /dev/null +++ b/connector_pms/models/queue_job.py @@ -0,0 +1,27 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class QueueJob(models.Model): + _inherit = "queue.job" + + pms_property_id = fields.Many2one( + comodel_name="pms.property", + string="Property", + store=True, + ) + + @api.depends("args") + def _compute_pms_property_id(self): + for rec in self: + if rec.args[1:2] and isinstance( + rec.args[1:2][0], type(self.env["pms.property"]) + ): + rec.pms_property_id = rec.args[1:2][0] + else: + rec.pms_property_id = False diff --git a/connector_pms/readme/CONTRIBUTORS.rst b/connector_pms/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..a9e48ccda7 --- /dev/null +++ b/connector_pms/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Eric Antones diff --git a/connector_pms/readme/DESCRIPTION.rst b/connector_pms/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..a034faa694 --- /dev/null +++ b/connector_pms/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +Base module for implement channel connectors + +Features: + + * Avaliability Management + * Odoo Connector diff --git a/connector_pms/readme/USAGE.rst b/connector_pms/readme/USAGE.rst new file mode 100644 index 0000000000..970d18f192 --- /dev/null +++ b/connector_pms/readme/USAGE.rst @@ -0,0 +1 @@ +No configuration required. This is a 'tool' module, need be used with other modules. diff --git a/connector_pms/security/ir.model.access.csv b/connector_pms/security/ir.model.access.csv new file mode 100644 index 0000000000..ad4ec7bb98 --- /dev/null +++ b/connector_pms/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_channel_backend_manager,channel backend manager,model_channel_backend,connector.group_connector_manager,1,1,1,1 +access_channel_backend_type_manager,channel backend type manager,model_channel_backend_type,connector.group_connector_manager,1,1,1,1 +access_channel_backend_method_manager,channel backend method manager,model_channel_backend_method,connector.group_connector_manager,1,1,1,1 +access_channel_backend_log_manager,channel backend log manager,model_channel_backend_log,connector.group_connector_manager,1,1,1,1 diff --git a/connector_pms/static/description/icon.png b/connector_pms/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/connector_pms/static/description/icon.png differ diff --git a/connector_pms/static/description/index.html b/connector_pms/static/description/index.html new file mode 100644 index 0000000000..c60213155c --- /dev/null +++ b/connector_pms/static/description/index.html @@ -0,0 +1,441 @@ + + + + + +PMS Connector + + + +
+

PMS Connector

+ + +

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

+

Base module for implement channel connectors

+

Features:

+
+
    +
  • Avaliability Management
  • +
  • Odoo Connector
  • +
+
+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Usage

+

No configuration required. This is a ‘tool’ module, need be used with other modules.

+
+
+

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

+ +
+
+

Contributors

+ +
+
+

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/connector_pms/views/channel_backend_log_views.xml b/connector_pms/views/channel_backend_log_views.xml new file mode 100644 index 0000000000..ea7c7950da --- /dev/null +++ b/connector_pms/views/channel_backend_log_views.xml @@ -0,0 +1,55 @@ + + + + + channel.backend.log.form + channel.backend.log + +
+
+ + + + + + + + + + + + + + + + + + + + channel.backend.log.tree + channel.backend.log + + + + + + + + + + + + + PMS Backend Log + channel.backend.log + + + + diff --git a/connector_pms/views/channel_backend_method_views.xml b/connector_pms/views/channel_backend_method_views.xml new file mode 100644 index 0000000000..30bb345265 --- /dev/null +++ b/connector_pms/views/channel_backend_method_views.xml @@ -0,0 +1,50 @@ + + + + + channel.backend.method.form + channel.backend.method + +
+
+ + + + + + + + + + + + + + + + channel.backend.method.tree + channel.backend.method + + + + + + + + + + + + PMS Backend Method + channel.backend.method + + + + diff --git a/connector_pms/views/channel_backend_type_views.xml b/connector_pms/views/channel_backend_type_views.xml new file mode 100644 index 0000000000..2d989f7f94 --- /dev/null +++ b/connector_pms/views/channel_backend_type_views.xml @@ -0,0 +1,70 @@ + + + + + channel.backend.type.form + channel.backend.type + +
+
+ + +
+ +
+
+ + + + + + channel.backend.type.tree + channel.backend.type + + + + + + + + + + + channel.backend.tree + channel.backend + + + + + + + + + diff --git a/pms/templates/pms_email_template.xml b/pms/templates/pms_email_template.xml new file mode 100644 index 0000000000..df7de0ebd4 --- /dev/null +++ b/pms/templates/pms_email_template.xml @@ -0,0 +1,154 @@ + + + + + + + + + + Property: Reservation Confirmed + + ${('%s <%s>' % (object.pms_property_id.partner_id.name, object.pms_property_id.partner_id.email) or '')|safe} + ${(object.email or '')|safe} + ${(object.partner_id.id or '')} + ${object.lang} + + Your reservation ${object.name} has been confirmed by the property staff + + + qweb + + + diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py new file mode 100644 index 0000000000..73105a86d2 --- /dev/null +++ b/pms/tests/__init__.py @@ -0,0 +1,44 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2017 Solucións Aloxa S.L. +# Alexandre Díaz +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## +from . import test_pms_reservation +from . import test_pms_pricelist +from . import test_pms_checkin_partner +from . import test_pms_sale_channel +from . import test_pms_folio +from . import test_pms_availability_plan_rules +from . import test_pms_room_type +from . import test_pms_room_type_class +from . import test_pms_board_service +from . import test_pms_wizard_massive_changes +from . import test_pms_booking_engine +from . import test_pms_res_users +from . import test_pms_room +from . import test_pms_folio_invoice +from . import test_pms_folio_sale_line +from . import test_pms_wizard_split_join_swap_reservation +from . import test_product_template +from . import test_pms_multiproperty +from . import test_shared_room + +# from . import test_automated_mails +from . import test_pms_service +from . import test_pms_tourist_tax diff --git a/pms/tests/common.py b/pms/tests/common.py new file mode 100644 index 0000000000..866ed57faf --- /dev/null +++ b/pms/tests/common.py @@ -0,0 +1,38 @@ +from odoo.tests import common + + +class TestPms(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.availability_plan1 = cls.env["pms.availability.plan"].create( + {"name": "Availability Plan 1"} + ) + cls.pricelist1 = cls.env["product.pricelist"].create( + { + "name": "Pricelist 1", + "availability_plan_id": cls.availability_plan1.id, + } + ) + cls.company1 = cls.env["res.company"].create( + { + "name": "Company 1", + } + ) + cls.pms_property1 = cls.env["pms.property"].create( + { + "name": "Property 1", + "company_id": cls.company1.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + cls.room_type_class1 = cls.env["pms.room.type.class"].create( + { + "name": "Room Type Class 1", + "default_code": "RTC1", + } + ) + for pricelist in cls.env["product.pricelist"].search([]): + if not pricelist.availability_plan_id: + pricelist.availability_plan_id = cls.availability_plan1.id + pricelist.is_pms_available = True diff --git a/pms/tests/test_automated_mails.py b/pms/tests/test_automated_mails.py new file mode 100644 index 0000000000..28b95d4dd8 --- /dev/null +++ b/pms/tests/test_automated_mails.py @@ -0,0 +1,591 @@ +from odoo.exceptions import UserError + +from .common import TestPms + + +class TestPmsAutomatedMails(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.template = cls.env["mail.template"].search( + [("name", "=", "Confirmed Reservation")] + ) + + def test_create_automated_action(self): + """ + Checks that an automated_action is created correctly when an + automated_mail is created. + --------------------- + An automated_mail is created and then it is verified that + the automated_action was created. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "creation", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertTrue( + auto_mail.automated_actions_id, "Automated action should be created " + ) + + def test_no_action_creation_before(self): + """ + Check that an automated mail cannot be created with action='creation' + and moment='before'. + ----------------------- + An automated mail is created with action = 'creation' and moment = 'before'. + Then it is verified that a UserError was thrown because an automated_mail with + these parameters cannot be created. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "creation", + "moment": "before", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT & ASSERT + with self.assertRaises( + UserError, + msg="It should not be allowed to create the automated mail " + "with action 'creation' and moment 'before' values", + ): + self.env["pms.automated.mails"].create(automated_mail_vals) + + def test_trigger_moment_in_act_creation(self): + """ + Check that when creating an automated mail with parameters + action = 'creation' and moment = 'in_act' the trigger of the + automated_action created is 'on_create'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "creation", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_create", + "The trigger of the automated action must be 'on_create'", + ) + + def test_trigger_moment_after_in_creation_action(self): + """ + Check that when creating an automated mail with parameters + action = 'creation' and moment = 'after' the trigger of the + automated_action created is 'on_time'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "creation", + "moment": "after", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "time": 1, + "time_type": "hour", + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_time", + "The trigger of the automated action must be 'on_time'", + ) + + def test_trigger_moment_in_act_in_write_action(self): + """ + Check that when creating an automated mail with parameters + action = 'write' and moment = 'in_act' the trigger of the + automated_action created is 'on_write'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "write", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_write", + "The trigger of the automated action must be 'on_write'", + ) + + def test_trigger_moment_after_in_write_action(self): + """ + Check that when creating an automated mail with parameters + action = 'write' and moment = 'after' the trigger of the + automated_action created is 'on_time'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "write", + "moment": "after", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "time": 1, + "time_type": "hour", + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_time", + "The trigger of the automated action must be 'on_time'", + ) + + def test_time_moment_before_in_checkin(self): + """ + Check that when creating an automated mail with parameters + action = 'checkin' and moment = 'before' the trg_date_range + of the automated_action created is equal to + (automated_mail.time * -1)'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkin", + "moment": "before", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "time": 60, + "time_type": "minutes", + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trg_date_range, + -60, + "The trg_date_range of the automated action must be '-60'", + ) + + def test_time_moment_in_act_in_checkin(self): + """ + Check that when creating an automated mail with parameters + action = 'checkin' and moment = 'in_act' the trg_date_range + of the automated_action created is equal to 0 + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkin", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trg_date_range, + 0, + "The trg_date_range of the automated action must be '0'", + ) + + def test_trigger_moment_before_in_checkin(self): + """ + Check that when creating an automated mail with parameters + action = 'checkin' and moment = 'before' the trigger of the + automated_action created is 'on_time'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkin", + "moment": "before", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "time": 24, + "time_type": "hour", + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_time", + "The trigger of the automated action must be 'on_time'", + ) + + def test_trigger_moment_in_act_in_checkin(self): + """ + Check that when creating an automated mail with parameters + action = 'checkin' and moment = 'in_act' the trigger of the + automated_action created is 'on_write'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkin", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_write", + "The trigger of the automated action must be 'on_write'", + ) + + def test_time_moment_in_act_in_checkout(self): + """ + Check that when creating an automated mail with parameters + action = 'checkout' and moment = 'in_act' the trg_date_range + of the automated_action created is equal to 0. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkout", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trg_date_range, + 0, + "The trg_date_range of the automated action must be '0'", + ) + + def test_trigger_moment_before_in_checkout(self): + """ + Check that when creating an automated mail with parameters + action = 'checkout' and moment = 'before' the trigger of the + automated_action created is 'on_time'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkout", + "moment": "before", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "time": 24, + "time_type": "hour", + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_time", + "The trigger of the automated action must be 'on_time'", + ) + + def test_trigger_moment_in_act_in_checkout(self): + """ + Check that when creating an automated mail with parameters + action = 'checkout' and moment = 'in_act' the trigger of the + automated_action created is 'on_write'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkout", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_write", + "The trigger of the automated action must be 'on_write'", + ) + + def test_trigger_moment_in_act_in_payment_action(self): + """ + Check that when creating an automated mail with parameters + action = 'payment' and moment = 'in_act' the trigger of the + automated_action created is 'on_create'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "payment", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_create", + "The trigger of the automated action must be 'on_create'", + ) + + def test_trigger_moment_before_in_payment_action(self): + """ + Check that when creating an automated mail with parameters + action = 'payment' and moment = 'before' the trigger of the + automated_action created is 'on_time'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "payment", + "moment": "before", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "time": 24, + "time_type": "hour", + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_time", + "The trigger of the automated action must be 'on_time'", + ) + + def test_time_moment_before_in_payment_action(self): + """ + Check that when creating an automated mail with parameters + action = 'payment' and moment = 'before' the trg_date_range + field of the automated_action is (automated_mail.time * -1). + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "payment", + "moment": "before", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "time": 24, + "time_type": "hour", + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trg_date_range, + -24, + "The trg_date_range of the automated action must be '-24'", + ) + + def test_trigger_moment_in_act_in_invoice_action(self): + """ + Check that when creating an automated mail with parameters + action = 'invoice' and moment = 'in_act' the trigger field + of the automated_action created is 'on_create'. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "invoice", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trigger, + "on_create", + "The trigger of the automated action must be 'on_create'", + ) + + def test_time_moment_in_act_in_invoice_action(self): + """ + Check that when creating an automated mail with parameters + action = 'invoice' and moment = 'in_act' the trg_date_range + field of the automated_action is 0. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "invoice", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.trg_date_range, + 0, + "The trg_date_range of the automated action must be '0'", + ) + + def test_filter_pre_domain_moment_in_act_in_checkin(self): + """ + Check that when creating an automated mail with parameters + action = 'checkin' and moment = 'in_act' the filter_pre_domain + field of the automated_action is [('state', '=', 'confirm')]. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkin", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.filter_pre_domain, + "[('state', '=', 'confirm')]", + "The filter_pre_domain of the automated action " + "must be '[('state', '=', 'confirm')]'", + ) + + def test_filter_domain_moment_in_act_in_checkin(self): + """ + Check that when creating an automated mail with parameters + action = 'checkin' and moment = 'in_act' the filter_domain + field of the automated_action is + [('state', '=', 'onboard'), ('pms_property_id', '=', [value of property_id.id])]]. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkin", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + pms_property_id_str = str(auto_mail.pms_property_ids.ids) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.filter_domain, + "[('state', '=', 'onboard'), ('pms_property_id', 'in', " + + pms_property_id_str + + ")]", + "The filter_domain of the automated action must be " + "'[('state', '=', 'onboard'), " + "('pms_property_id', '=', [value of property_id.id])]'", + ) + + def test_filter_pre_domain_moment_in_act_in_checkout(self): + """ + Check that when creating an automated mail with parameters + action = 'checkout' and moment = 'in_act' the filter_pre_domain + field of the automated_action is [('state', '=', 'onboard')]. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkout", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.filter_pre_domain, + "[('state', '=', 'onboard')]", + "The filter_pre_domain of the automated action must " + "be '[('state', '=', 'onboard')]'", + ) + + def test_filter_domain_moment_in_act_in_checkout(self): + """ + Check that when creating an automated mail with parameters + action = 'checkout' and moment = 'in_act' the filter_domain + field of the automated_action is + [('state', '=', 'out'), ('pms_property_id', '=', [value of property_id.id])]]. + """ + # ARRANGE + automated_mail_vals = { + "name": "Auto Mail 1", + "template_id": self.template.id, + "action": "checkout", + "moment": "in_act", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + + # ACT + auto_mail = self.env["pms.automated.mails"].create(automated_mail_vals) + pms_property_id_str = str(auto_mail.pms_property_ids.ids) + + # ASSERT + self.assertEqual( + auto_mail.automated_actions_id.filter_domain, + "[('state', '=', 'done'), ('pms_property_id', 'in', " + + pms_property_id_str + + ")]", + "The filter_pre_domain of the automated action must " + "be '[('state', '=', 'out'), ('pms_property_id', '=', [value of property_id.id])]", + ) diff --git a/pms/tests/test_pms_availability_plan_rules.py b/pms/tests/test_pms_availability_plan_rules.py new file mode 100644 index 0000000000..528ad9c364 --- /dev/null +++ b/pms/tests/test_pms_availability_plan_rules.py @@ -0,0 +1,629 @@ +import datetime + +from odoo import fields +from odoo.exceptions import ValidationError + +from .common import TestPms + + +class TestPmsRoomTypeAvailabilityRules(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.pms_property2 = cls.env["pms.property"].create( + { + "name": "Property 2", + "company_id": cls.company1.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + cls.pricelist2 = cls.env["product.pricelist"].create( + { + "name": "test pricelist 1", + "pms_property_ids": [ + (4, cls.pms_property1.id), + (4, cls.pms_property2.id), + ], + "availability_plan_id": cls.availability_plan1.id, + "is_pms_available": True, + } + ) + # pms.sale.channel + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + # pms.availability.plan + cls.test_room_type_availability1 = cls.env["pms.availability.plan"].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [cls.pricelist2.id])], + } + ) + # pms.property + cls.pms_property3 = cls.env["pms.property"].create( + { + "name": "MY PMS TEST", + "company_id": cls.company1.id, + "default_pricelist_id": cls.pricelist2.id, + } + ) + cls.pricelist2.write( + { + "pms_property_ids": [ + (4, cls.pms_property3.id), + ], + } + ) + + # pms.room.type + cls.test_room_type_single = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property3.id], + "name": "Single Test", + "default_code": "SNG_Test", + "class_id": cls.room_type_class1.id, + } + ) + # pms.room.type + cls.test_room_type_double = cls.env["pms.room.type"].create( + { + "pms_property_ids": [ + (4, cls.pms_property3.id), + ], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + } + ) + # pms.room + cls.test_room1_double = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property3.id, + "name": "Double 201 test", + "room_type_id": cls.test_room_type_double.id, + "capacity": 2, + } + ) + # pms.room + cls.test_room2_double = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property3.id, + "name": "Double 202 test", + "room_type_id": cls.test_room_type_double.id, + "capacity": 2, + } + ) + cls.test_room1_single = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property3.id, + "name": "Single 101 test", + "room_type_id": cls.test_room_type_single.id, + "capacity": 1, + } + ) + # pms.room + cls.test_room2_single = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property3.id, + "name": "Single 102 test", + "room_type_id": cls.test_room_type_single.id, + "capacity": 1, + } + ) + # partner + cls.partner1 = cls.env["res.partner"].create( + {"name": "Charles", "property_product_pricelist": cls.pricelist1} + ) + + def test_availability_rooms_all(self): + """ + Check the availability of rooms in a property with an availability plan without + availability rules. + --------------------- + The checkin and checkout dates on which the availability will be checked are saved + in a variable and in another all the rooms of the property are also saved. Then the + free_room_ids compute field is called which should return the number of available rooms + of the property and they are saved in another variable with which it is verified that + all the rooms have been returned because there are no availability rules for that plan. + """ + + # ARRANGE + checkin = fields.date.today() + checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date() + test_rooms_double_rooms = self.env["pms.room"].search( + [("pms_property_id", "=", self.pms_property3.id)] + ) + # ACT + pms_property = self.pms_property3.with_context( + checkin=checkin, + checkout=checkout, + ) + result = pms_property.free_room_ids + + # ASSERT + obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms) + self.assertTrue( + obtained, + "Availability should contain the test rooms" + "because there's no availability rules for them.", + ) + + def test_plan_avail_update_to_one(self): + """ + Check that the plan avail on a room is updated when the real avail is changed + -------------------------------------------------------------- + Room type with 2 rooms, new reservation with this room type, the plan avail + must to be updated to 1. You must know that the pricelist2 is linked + with the plan test_room_type_availability1 + """ + # ARRANGE + checkin = fields.date.today() + checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date() + room_type = self.test_room_type_double + + self.test_room_type_availability_rule1 = self.env[ + "pms.availability.plan.rule" + ].create( + { + "availability_plan_id": self.test_room_type_availability1.id, + "room_type_id": self.test_room_type_double.id, + "date": fields.datetime.today(), + "pms_property_id": self.pms_property3.id, + } + ) + # ACT + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property3.id, + "checkin": checkin, + "checkout": checkout, + "partner_id": self.partner1.id, + "room_type_id": room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + result = self.test_room_type_availability_rule1.plan_avail + + # ASSERT + self.assertEqual( + result, + 1, + "There should be only one room in the result of the availability plan" + "because the real avail is 1 and the availability plan" + "is updated.", + ) + + def test_plan_avail_update_to_zero(self): + """ + Check that the plan avail on a room is updated when the real avail is changed + with real avail 0 + -------------------------------------------------------------- + Room type with 2 rooms, two new reservations with this room type, the plan avail + must to be updated to 0. You must know that the pricelist2 is linked + with the plan test_room_type_availability1 + """ + # ARRANGE + checkin = fields.date.today() + checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date() + room_type = self.test_room_type_double + + self.test_room_type_availability_rule1 = self.env[ + "pms.availability.plan.rule" + ].create( + { + "availability_plan_id": self.test_room_type_availability1.id, + "room_type_id": self.test_room_type_double.id, + "date": fields.datetime.today(), + "pms_property_id": self.pms_property3.id, + } + ) + # ACT + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property3.id, + "checkin": checkin, + "checkout": checkout, + "partner_id": self.partner1.id, + "room_type_id": room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property3.id, + "checkin": checkin, + "checkout": checkout, + "partner_id": self.partner1.id, + "room_type_id": room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + result = self.test_room_type_availability_rule1.plan_avail + + # ASSERT + self.assertEqual( + result, + 0, + "There should be zero in the result of the availability plan" + "because the real avail is 0 and the availability plan" + "is updated.", + ) + + def test_availability_rooms_all_lines(self): + """ + Check the availability of rooms in a property with an availability plan without + availability rules and passing it the reservation lines of a reservation for that + property. + ----------------- + The checkin and checkout dates on which the availability will be checked are saved + in a variable and in another all the rooms of the property are also saved. Then create + a reservation for this property and the free_room_ids compute field is called with the + parameters checkin, checkout and the reservation lines of the reservation as a curent + lines, this method should return the number of available rooms of the property. Then the + result is saved in another variable with which it is verified that all the rooms have + been returned because there are no availability rules for that plan. + """ + + # ARRANGE + checkin = fields.date.today() + checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date() + test_rooms_double_rooms = self.env["pms.room"].search( + [("pms_property_id", "=", self.pms_property3.id)] + ) + test_reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property3.id, + "checkin": checkin, + "checkout": checkout, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + # REVIEW: reservation without room and wihout room type? + pms_property = self.pms_property3.with_context( + checkin=checkin, + checkout=checkout, + current_lines=test_reservation.reservation_line_ids.ids, + ) + result = pms_property.free_room_ids + + # ASSERT + obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms) + self.assertTrue( + obtained, + "Availability should contain the test rooms" + "because there's no availability rules for them.", + ) + + def test_availability_rooms_room_type(self): + """ + Check the availability of a room type for a property. + ---------------- + Double rooms of a property are saved in a variable. The free_room_ids compute field + is called giving as parameters checkin, checkout and the type of room (in this + case double). Then with the all () function we check that all rooms of this type + were returned. + """ + + # ARRANGE + test_rooms_double_rooms = self.env["pms.room"].search( + [ + ("pms_property_id", "=", self.pms_property3.id), + ("room_type_id", "=", self.test_room_type_double.id), + ] + ) + # ACT + pms_property = self.pms_property3.with_context( + checkin=fields.date.today(), + checkout=(fields.datetime.today() + datetime.timedelta(days=4)).date(), + room_type_id=self.test_room_type_double.id, + ) + result = pms_property.free_room_ids + + # ASSERT + obtained = all(elem.id in result.ids for elem in test_rooms_double_rooms) + self.assertTrue( + obtained, + "Availability should contain the test rooms" + "because there's no availability rules for them.", + ) + + def test_availability_closed_no_room_type(self): + """ + Check that rooms of a type with an availability rule with closed = True are + not available on the dates marked in the date field of the availability rule. + -------------------- + Create an availability rule for double rooms with the field closed = true + and the date from today until tomorrow. Then the availability is saved in a + variable through the free_room_ids computed field, passing it the pricelist that + it contains the availability plan where the rule is included, and the checkin + and checkout dates are between the date of the rule. Then it is verified that + the double rooms are not available. + """ + # ARRANGE + self.test_room_type_availability_rule1 = self.env[ + "pms.availability.plan.rule" + ].create( + { + "availability_plan_id": self.test_room_type_availability1.id, + "room_type_id": self.test_room_type_double.id, + "date": (fields.datetime.today() + datetime.timedelta(days=2)).date(), + "closed": True, # <- (1/2) + "pms_property_id": self.pms_property3.id, + } + ) + # ACT + pms_property = self.pms_property3.with_context( + checkin=fields.date.today(), + checkout=(fields.datetime.today() + datetime.timedelta(days=4)).date(), + # room_type_id=False, # <- (2/2) + pricelist_id=self.pricelist2.id, + ) + result = pms_property.free_room_ids + + # ASSERT + self.assertNotIn( + self.test_room_type_double, + result.mapped("room_type_id"), + "Availability should not contain rooms of a type " + "which its availability rules applies", + ) + + def test_availability_rules(self): + """ + Check through subtests that the availability rules are applied + for a specific room type. + ---------------- + Test cases: + 1. closed_arrival = True + 2. closed_departure = True + 3. min_stay = 5 + 4. max_stay = 2 + 5. min_stay_arrival = 5 + 6. max_stay_arrival = 3 + 7. quota = 0 + 8. max_avail = 0 + For each test case, it is verified through the free_room_ids compute field, + that double rooms are not available since the rules are applied to this + room type. + """ + + # ARRANGE + + self.test_room_type_availability_rule1 = self.env[ + "pms.availability.plan.rule" + ].create( + { + "availability_plan_id": self.test_room_type_availability1.id, + "room_type_id": self.test_room_type_double.id, + "date": fields.date.today(), + "pms_property_id": self.pms_property3.id, + } + ) + + checkin = fields.date.today() + checkout = (fields.datetime.today() + datetime.timedelta(days=4)).date() + + test_cases = [ + { + "closed": False, + "closed_arrival": True, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": True, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkout, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 5, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 2, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 5, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 3, + "quota": -1, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": 0, + "max_avail": -1, + "date": checkin, + }, + { + "closed": False, + "closed_arrival": False, + "closed_departure": False, + "min_stay": 0, + "max_stay": 0, + "min_stay_arrival": 0, + "max_stay_arrival": 0, + "quota": -1, + "max_avail": 0, + "date": checkin, + }, + ] + + for test_case in test_cases: + with self.subTest(k=test_case): + + # ACT + self.test_room_type_availability_rule1.write(test_case) + + pms_property = self.pms_property3.with_context( + checkin=checkin, + checkout=checkout, + room_type_id=self.test_room_type_double.id, + pricelist_id=self.pricelist2.id, + ) + result = pms_property.free_room_ids + + # ASSERT + self.assertNotIn( + self.test_room_type_double, + result.mapped("room_type_id"), + "Availability should not contain rooms of a type " + "which its availability rules applies", + ) + + def test_rule_on_create_reservation(self): + """ + Check that a reservation is not created when an availability rule prevents it . + ------------------- + Create an availability rule for double rooms with the + field closed = True and the date from today until tomorrow. Then try to create + a reservation for that type of room with a checkin date today and a checkout + date within 4 days. This should throw a ValidationError since the rule does + not allow creating reservations for those dates. + """ + + # ARRANGE + self.test_room_type_availability_rule1 = self.env[ + "pms.availability.plan.rule" + ].create( + { + "availability_plan_id": self.test_room_type_availability1.id, + "room_type_id": self.test_room_type_double.id, + "date": (fields.datetime.today() + datetime.timedelta(days=2)).date(), + "closed": True, + "pms_property_id": self.pms_property3.id, + } + ) + checkin = datetime.datetime.now() + checkout = datetime.datetime.now() + datetime.timedelta(days=4) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Availability rules should be applied that would" + " prevent the creation of the reservation.", + ): + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property3.id, + "checkin": checkin, + "checkout": checkout, + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "pricelist_id": self.pricelist2.id, + "partner_id": self.partner1.id, + } + ) + + def test_rule_update_quota_on_create_reservation(self): + """ + Check that the availability rule with quota = 1 for a room + type does not allow you to create more reservations than 1 + for that room type. + """ + + # ARRANGE + + self.test_room_type_availability_rule1 = self.env[ + "pms.availability.plan.rule" + ].create( + { + "availability_plan_id": self.test_room_type_availability1.id, + "room_type_id": self.test_room_type_double.id, + "date": datetime.date.today(), + "quota": 1, + "pms_property_id": self.pms_property3.id, + } + ) + self.pricelist2.pms_property_ids = [ + (4, self.pms_property1.id), + (4, self.pms_property2.id), + (4, self.pms_property3.id), + ] + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property3.id, + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "pricelist_id": self.pricelist2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + with self.assertRaises( + ValidationError, + msg="The quota shouldnt be enough to create a new reservation", + ): + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property3.id, + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "pricelist_id": self.pricelist2.id, + "partner_id": self.partner1.id, + } + ) diff --git a/pms/tests/test_pms_board_service.py b/pms/tests/test_pms_board_service.py new file mode 100644 index 0000000000..43cf24839c --- /dev/null +++ b/pms/tests/test_pms_board_service.py @@ -0,0 +1,411 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError + +from .common import TestPms + + +class TestBoardService(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company2 = cls.env["res.company"].create( + { + "name": "Company 2", + } + ) + cls.pms_property3 = cls.env["pms.property"].create( + { + "name": "Property 3", + "company_id": cls.company2.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + + def test_create_bs_one_company_inconsistent_code(self): + """ + Creation of board service with the same code as an existing one + belonging to the same property should fail. + + PRE: - board service bs1 exists + - board_service1 has code c1 + - board_service1 has pms_property1 + - pms_property1 has company company1 + ACT: - create a new board_service2 + - board_service2 has code c1 + - board_service2 has pms_property1 + - pms_property1 has company company1 + POST: - Integrity error: the room type already exists + - board_service2 not created + """ + # ARRANGE + # board_service1 + self.env["pms.board.service"].create( + { + "name": "Board service bs1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="The board service has been created and it shouldn't" + ): + # board_service2 + self.env["pms.board.service"].create( + { + "name": "Board service bs2", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + + def test_create_bs_several_companies_inconsistent_code(self): + """ + Creation of board service with properties and one of its + properties has the same code on its board services should fail. + + PRE: - board service bs1 exists + - board_service1 has code c1 + - board_service1 has property pms_property1 + - pms_property1 has company company1 + ACT: - create a new board_service2 + - board_service2 has code c1 + - board_service2 has property pms_property1, pms_property2, + pms_property3 + - pms_property1, pms_property2 has company company1 + - pms_property3 has company company2 + POST: - Integrity error: the board service already exists + - board_service2 not created + """ + # ARRANGE + self.pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + # board_service1 + self.env["pms.board.service"].create( + { + "name": "Board service 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="The board service has been created and it shouldn't" + ): + # board_service2 + self.env["pms.board.service"].create( + { + "name": "Board service bs2", + "default_code": "c1", + "pms_property_ids": [ + ( + 6, + 0, + [ + self.pms_property1.id, + self.pms_property2.id, + self.pms_property3.id, + ], + ) + ], + } + ) + + def test_search_bs_code_same_company_several_properties(self): + """ + Checks the search for a board service by code when the board service + belongs to properties of the same company + + PRE: - board service bs1 exists + - board_service1 has code c1 + - board_service1 has 2 properties pms_property1 and pms_property2 + - pms_property_1 and pms_property2 have the same company company1 + ACT: - search board service with code c1 and pms_property1 + - pms_property1 has company company1 + POST: - only board_service1 board service found + """ + # ARRANGE + self.pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + board_service1 = self.env["pms.board.service"].create( + { + "name": "Board service 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property2.id]) + ], + } + ) + # ACT + board_services = self.env["pms.board.service"].get_unique_by_property_code( + self.pms_property1.id, "c1" + ) + # ASSERT + self.assertEqual( + board_services.id, + board_service1.id, + "Expected board service not found", + ) + + def test_search_bs_code_several_companies_several_properties_not_found(self): + """ + Checks the search for a board service by code when the board service + belongs to properties with different companies + + PRE: - board service bs1 exists + - board_service1 has code c1 + - board_service1 has 2 properties pms_property1 and pms_property3 + - pms_property1 and pms_property3 have different companies + - pms_property1 have company company1 and pms_property3 have company2 + ACT: - search board service with code c1 and property pms_property1 + - pms_property1 has company company1 + POST: - only board_service1 room type found + """ + # ARRANGE + bs1 = self.env["pms.board.service"].create( + { + "name": "Board service 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property3.id]) + ], + } + ) + # ACT + board_services = self.env["pms.board.service"].get_unique_by_property_code( + self.pms_property1.id, "c1" + ) + # ASSERT + self.assertEqual(board_services.id, bs1.id, "Expected board service not found") + + def test_search_bs_code_no_result(self): + """ + Search for a specific board service code and its property. + The board service exists but not in the property given. + + PRE: - board_service1 exists + - board_service1 has code c1 + - board_service1 with 2 properties pms_property1 and pms_property2 + - pms_property1 and pms_property2 have same company company1 + ACT: - search board service with code c1 and property pms_property3 + - pms_property3 have company company2 + POST: - no room type found + """ + # ARRANGE + self.pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + # board_service1 + self.env["pms.board.service"].create( + { + "name": "Board service 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property2.id]) + ], + } + ) + # ACT + board_services = self.env["pms.board.service"].get_unique_by_property_code( + self.pms_property3.id, "c1" + ) + # ASSERT + self.assertFalse( + board_services, "Board service found but it should not have found any" + ) + + def test_search_bs_code_present_all_companies_and_properties(self): + """ + Search for a specific board service and its property. + The board service exists without property, then + the search foundS the result. + + PRE: - board_service1 exists + - board_service1 has code c1 + - board_service1 properties are null + ACT: - search board service with code c1 and property pms_property1 + - pms_property1 have company company1 + POST: - only board_service1 board service found + """ + # ARRANGE + board_service1 = self.env["pms.board.service"].create( + { + "name": "Board service 1", + "default_code": "c1", + "pms_property_ids": False, + } + ) + # ACT + board_services = self.env["pms.board.service"].get_unique_by_property_code( + self.pms_property1.id, "c1" + ) + # ASSERT + self.assertEqual( + board_services.id, + board_service1.id, + "Expected board service not found", + ) + + def test_search_bs_code_several_companies_several_properties(self): + """ + Search for a specific board service and its property. + There is one board service without properties and + another one with the same code that belongs to 2 properties + (from different companies) + The search founds only the board service that match the + property given. + + PRE: - board_service1 exists + - board_service1 has code c1 + - board_service1 has 2 properties pms_property1 and pms_property3 + - pms_property1 and pms_property2 have the same company company1 + - board service board_service2 exists + - board_service2 has code c1 + - board_service2 has no properties + ACT: - search board service with code c1 and property pms_property1 + - pms_property1 have company company1 + POST: - only board_service1 board service found + """ + # ARRANGE + board_service1 = self.env["pms.board.service"].create( + { + "name": "Board service 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property3.id]) + ], + } + ) + # board_service2 + self.env["pms.board.service"].create( + { + "name": "Board service bs2", + "default_code": "c1", + "pms_property_ids": False, + } + ) + # ACT + board_services = self.env["pms.board.service"].get_unique_by_property_code( + self.pms_property1.id, "c1" + ) + # ASSERT + self.assertEqual( + board_services.id, + board_service1.id, + "Expected board service not found", + ) + + def test_search_bs_code_same_companies_several_properties(self): + """ + Search for a specific board service and its property. + There is one board service without properties and + another one with the same code that belongs to 2 properties + (same company). + The search founds only the board service that match the + property given. + + PRE: - board_service1 exists + - board_service1 has code c1 + - board_service1 has property pms_property1 + - pms_property1 have the company company1 + - board service board_service2 exists + - board_service2 has code c1 + - board_service2 has no properties + ACT: - search board service with code c1 and pms_property2 + - pms_property2 have company company1 + POST: - only board_service2 board service found + """ + # ARRANGE + self.pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + self.env["pms.board.service"].create( + { + "name": "Board service 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + board_service2 = self.env["pms.board.service"].create( + { + "name": "Board service bs2", + "default_code": "c1", + "pms_property_ids": False, + } + ) + # ACT + board_services = self.env["pms.board.service"].get_unique_by_property_code( + self.pms_property2.id, "c1" + ) + # ASSERT + self.assertEqual( + board_services.id, + board_service2.id, + "Expected board service not found", + ) + + def test_search_bs_code_no_properties(self): + """ + Search for a specific board service and its property. + There is one board service without properties and + another one with the same code that belongs to one property. + The search founds only the board service that match the + property given that it's not the same as the 2nd one. + + PRE: - board_service1 exists + - board_service1 has code c1 + - board_service1 has property pms_property1 + - pms_property1 have the company company1 + - board service board_service2 exists + - board_service2 has code c1 + - board_service2 has no properties + ACT: - search board service with code c1 and property pms_property3 + - pms_property3 have company company2 + POST: - only board_service2 board service found + """ + # ARRANGE + # board_service1 + self.env["pms.board.service"].create( + { + "name": "Board service bs1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + board_service2 = self.env["pms.board.service"].create( + { + "name": "Board service bs2", + "default_code": "c1", + "pms_property_ids": False, + } + ) + # ACT + board_services = self.env["pms.board.service"].get_unique_by_property_code( + self.pms_property3.id, "c1" + ) + # ASSERT + self.assertEqual( + board_services.id, + board_service2.id, + "Expected board service not found", + ) diff --git a/pms/tests/test_pms_booking_engine.py b/pms/tests/test_pms_booking_engine.py new file mode 100644 index 0000000000..2f85382402 --- /dev/null +++ b/pms/tests/test_pms_booking_engine.py @@ -0,0 +1,1010 @@ +import datetime + +from freezegun import freeze_time + +from odoo import fields + +from .common import TestPms + + +class TestPmsBookingEngine(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + # CREATION OF ROOM TYPE (WITH ROOM TYPE CLASS) + cls.test_room_type_double = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + "list_price": 40.0, + } + ) + + # pms.room + cls.test_room1_double = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 201 test", + "room_type_id": cls.test_room_type_double.id, + "capacity": 2, + } + ) + + # pms.room + cls.test_room2_double = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 202 test", + "room_type_id": cls.test_room_type_double.id, + "capacity": 2, + } + ) + + # pms.room + cls.test_room3_double = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 203 test", + "room_type_id": cls.test_room_type_double.id, + "capacity": 2, + } + ) + + # pms.room + cls.test_room4_double = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 204 test", + "room_type_id": cls.test_room_type_double.id, + "capacity": 2, + } + ) + + # res.partner + cls.partner_id = cls.env["res.partner"].create( + { + "name": "Miguel", + "mobile": "654667733", + "email": "miguel@example.com", + } + ) + + # pms.sale.channel + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + + def test_price_wizard_correct(self): + # TEST CASE + """ + Check by subtests if the total_price field is applied correctly + with and without discount. + ------------ + Create two test cases: one with the discount at 0 and with the + expected total price, which is the difference in days between + checkin and checkout, multiplied by the room price and multiplied + by the number of rooms, and another with the discount at 0.5 and with + total price the same as the first. Then the wizard is created and it + is verified that the wizard's total_price_folio is the same as the + expected price. + """ + + # ARRANGE + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + days = (checkout - checkin).days + num_double_rooms = 4 + discounts = [ + { + "discount": 0, + "expected_price": days + * self.test_room_type_double.list_price + * num_double_rooms, + }, + { + "discount": 0.5, + "expected_price": ( + days * self.test_room_type_double.list_price * num_double_rooms + ) + * 0.5, + }, + ] + + # create folio wizard with partner id => pricelist & start-end dates + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + + # force pricelist load + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.pms_property1.id), + ] + ) + + # set value for room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", num_double_rooms), + ] + ) + + lines_availability_test[0].num_rooms_selected = value + for discount in discounts: + with self.subTest(k=discount): + # ACT + booking_engine.discount = discount["discount"] + + # ASSERT + self.assertEqual( + booking_engine.total_price_folio, + discount["expected_price"], + "The total price calculation is wrong", + ) + + def test_price_wizard_correct_pricelist_applied(self): + """ + Check that the total_price field is applied correctly in + the wizard(pricelist applied). + ------------------ + Create a pricelist item for pricelist1 and a wizard is also + created with pricelist1. Then it is verified that the value + of the total price of the wizard corresponds to the value of + the price of the pricelist item. + """ + + # ARRANGE + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + days = (checkout - checkin).days + + # num. rooms of type double to book + num_double_rooms = 4 + + # price for today + price_today = 38.0 + + # expected price + expected_price_total = days * price_today * num_double_rooms + + # set pricelist item for current day + product_tmpl = self.test_room_type_double.product_id.product_tmpl_id + self.env["product.pricelist.item"].create( + { + "pricelist_id": self.pricelist1.id, + "date_start_consumption": checkin, + "date_end_consumption": checkin, + "compute_price": "fixed", + "applied_on": "1_product", + "product_tmpl_id": product_tmpl.id, + "product_id": self.test_room_type_double.product_id.id, + "fixed_price": price_today, + "min_quantity": 0, + "pms_property_ids": product_tmpl.pms_property_ids.ids, + } + ) + + # create folio wizard with partner id => pricelist & start-end dates + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.pms_property1.id), + ] + ) + + # set value for room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", num_double_rooms), + ] + ) + + # ACT + lines_availability_test[0].num_rooms_selected = value + + # ASSERT + self.assertEqual( + booking_engine.total_price_folio, + expected_price_total, + "The total price calculation is wrong", + ) + + # REVIEW: This test is set to check min qty, but the workflow price, actually, + # always is set to 1 qty and the min_qty cant be applied. + # We could set qty to number of rooms?? + + # def test_price_wizard_correct_pricelist_applied_min_qty_applied(self): + # # TEST CASE + # # Set values for the wizard and the total price is correct + # # (pricelist applied) + + # # ARRANGE + # # common scenario + # self.create_common_scenario() + + # # checkin & checkout + # checkin = fields.date.today() + # checkout = fields.date.today() + datetime.timedelta(days=1) + # days = (checkout - checkin).days + + # # set pricelist item for current day + # product_tmpl_id = self.test_room_type_double.product_id.product_tmpl_id.id + # pricelist_item = self.env["product.pricelist.item"].create( + # { + # "pricelist_id": self.test_pricelist.id, + # "date_start_consumption": checkin, + # "date_end_consumption": checkin, + # "compute_price": "fixed", + # "applied_on": "1_product", + # "product_tmpl_id": product_tmpl_id, + # "fixed_price": 38.0, + # "min_quantity": 4, + # } + # ) + + # # create folio wizard with partner id => pricelist & start-end dates + # booking_engine = self.env["pms.booking.engine"].create( + # { + # "start_date": checkin, + # "end_date": checkout, + # "partner_id": self.partner_id.id, + # "pricelist_id": self.test_pricelist.id, + # } + # ) + + # # availability items belonging to test property + # lines_availability_test = self.env["pms.folio.availability.wizard"].search( + # [ + # ("room_type_id.pms_property_ids", "in", self.test_property.id), + # ] + # ) + + # test_cases = [ + # { + # "num_rooms": 3, + # "expected_price": 3 * self.test_room_type_double.list_price * days, + # }, + # {"num_rooms": 4, "expected_price": 4 * pricelist_item.fixed_price * days}, + # ] + # import wdb; wdb.set_trace() + # for tc in test_cases: + # with self.subTest(k=tc): + # # ARRANGE + # # set value for room type double + # value = self.env["pms.num.rooms.selection"].search( + # [ + # ("room_type_id", "=", self.test_room_type_double.id), + # ("value", "=", tc["num_rooms"]), + # ] + # ) + # # ACT + # lines_availability_test[0].num_rooms_selected = value + + # # ASSERT + # self.assertEqual( + # booking_engine.total_price_folio, + # tc["expected_price"], + # "The total price calculation is wrong", + # ) + + def test_check_create_folio(self): + """ + Test that a folio is created correctly from the booking engine wizard. + ------------ + The wizard is created with a partner_id, a pricelist, and start and end + dates for property1. The availability items are searched for that property + and in the first one a double room is set. The create_folio() method of the + wizard is launched. The folios of the partner_id entered in the wizard are + searched and it is verified that the folio exists. + """ + + # ARRANGE + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + # create folio wizard with partner id => pricelist & start-end dates + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.pms_property1.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + + # ACT + booking_engine.create_folio() + + # ASSERT + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + + self.assertTrue(folio, "Folio not created.") + + def test_check_create_reservations(self): + """ + Check that reservations are created correctly from the booking engine wizard. + ------------ + The wizard is created with a partner_id, a pricelist, and start and end + dates for property1. The availability items are searched for that property + and in the first one, two double rooms are set, which create two reservations + too. The create_folio() method of the wizard is launched. The folios of the + partner_id entered in the wizard are searched and it is verified that the two + reservations of the folio was created. + """ + + # ARRANGE + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + # create folio wizard with partner id => pricelist & start-end dates + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.pms_property1.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", 2), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 2 + + # ACT + booking_engine.create_folio() + + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + + # ASSERT + self.assertEqual(len(folio.reservation_ids), 2, "Reservations not created.") + + def test_values_folio_created(self): + """ + Check that the partner_id and pricelist_id values of the folio correspond + to the partner_id and pricelist_id of the booking engine wizard that created + the folio. + ----------- + The wizard is created with a partner_id, a pricelist, and start and end + dates for property1. The availability items are searched for that property + and in the first one a double room are set. The create_folio() method of the + wizard is launched. Then it is checked that the partner_id and the pricelist_id + of the created folio are the same as the partner_id and the pricelist_id of the + booking engine wizard. + """ + + # ARRANGE + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + # create folio wizard with partner id => pricelist & start-end dates + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.pms_property1.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 1 + + # ACT + booking_engine.create_folio() + vals = { + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + } + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + + # ASSERT + for key in vals: + with self.subTest(k=key): + self.assertEqual( + folio[key].id, + vals[key], + "The value of " + key + " is not correctly established", + ) + + def test_values_reservation_created(self): + """ + Check with subtests that the values of the fields of a reservation created through + a booking engine wizard are correct. + -------------- + The wizard is created with a partner_id, a pricelist, and start and end + dates for property1. The availability items are searched for that property + and in the first one a double room are set. The create_folio() method of the + wizard is launched. A vals dictionary is created with folio_id, checkin and + checkout, room_type_id, partner_id, pricelist_id, and pms_property_id. Then + the keys of this dictionary are crossed and it is verified that the values + correspond with the values of the reservation created from the wizard . + """ + + # ARRANGE + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + # create folio wizard with partner id => pricelist & start-end dates + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.pms_property1.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 1 + + # ACT + booking_engine.create_folio() + + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + + vals = { + "folio_id": folio.id, + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.test_room_type_double.id, + "partner_id": self.partner_id.id, + "pricelist_id": folio.pricelist_id.id, + "pms_property_id": self.pms_property1.id, + } + + # ASSERT + for reservation in folio.reservation_ids: + for key in vals: + with self.subTest(k=key): + self.assertEqual( + reservation[key].id + if key + in [ + "folio_id", + "partner_id", + "pricelist_id", + "pms_property_id", + "room_type_id", + ] + else reservation[key], + vals[key], + "The value of " + key + " is not correctly established", + ) + + def test_reservation_line_discounts(self): + """ + Check that a discount applied to a reservation from a booking engine wizard + is applied correctly in the reservation line. + ----------------- + The wizard is created with a partner_id, a pricelist, a discount of 0.5 and + start and end dates for property1. The availability items are searched for + that property and in the first one a double room are set. The create_folio() + method of the wizard is launched.Then it is verified that the discount of the + reservation line is equal to the discount applied in the wizard. + """ + + # ARRANGE + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + discount = 0.5 + + # create folio wizard with partner id => pricelist & start-end dates + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "discount": discount, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.pms_property1.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 1 + + # ACT + booking_engine.create_folio() + + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + + # ASSERT + for line in folio.reservation_ids.reservation_line_ids: + with self.subTest(k=line): + self.assertEqual( + line.discount, + discount * 100, + "The discount is not correctly established", + ) + + def test_check_quota_avail(self): + """ + Check that the availability for a room type in the booking engine + wizard is correct by creating an availability_plan_rule with quota. + ----------------- + An availability_plan_rule with quota = 1 is created for the double + room type. A booking engine wizard is created with the checkin same + date as the availability_plan_rule and with pricelist1, which also has + the availability_plan set that contains the availability_plan_rule + created before. Then the availability is searched for the type of + double room which must be 1 because the availavility_plan_rule quota + for that room is 1. + """ + + # ARRANGE + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + self.availability_plan1 = self.env["pms.availability.plan"].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [self.pricelist1.id])], + } + ) + self.env["pms.availability.plan.rule"].create( + { + "quota": 1, + "room_type_id": self.test_room_type_double.id, + "availability_plan_id": self.availability_plan1.id, + "date": fields.date.today(), + "pms_property_id": self.pms_property1.id, + } + ) + # create folio wizard with partner id => pricelist & start-end dates + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + room_type_plan_avail = booking_engine.availability_results.filtered( + lambda r: r.room_type_id.id == self.test_room_type_double.id + ).num_rooms_available + + # ASSERT + + self.assertEqual(room_type_plan_avail, 1, "Quota not applied in Wizard Folio") + + def test_check_min_stay_avail(self): + """ + Check that the availability for a room type in the booking engine + wizard is correct by creating an availability_plan_rule with min_stay. + ----------------- + An availability_plan_rule with min_stay = 3 is created for the double + room type. A booking engine wizard is created with start_date = today + and end_date = tomorrow. Then the availability is searched for the type of + double room which must be 0 because the availability_plan_rule establishes + that the min_stay is 3 days and the difference of days in the booking engine + wizard is 1 . + """ + + # ARRANGE + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + # AVAILABILITY PLAN CREATION + self.availability_plan1 = self.env["pms.availability.plan"].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [self.pricelist1.id])], + } + ) + self.env["pms.availability.plan.rule"].create( + { + "min_stay": 3, + "room_type_id": self.test_room_type_double.id, + "availability_plan_id": self.availability_plan1.id, + "date": fields.date.today(), + "pms_property_id": self.pms_property1.id, + } + ) + + # create folio wizard with partner id => pricelist & start-end dates + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + room_type_plan_avail = booking_engine.availability_results.filtered( + lambda r: r.room_type_id.id == self.test_room_type_double.id + ).num_rooms_available + + # ASSERT + + self.assertEqual(room_type_plan_avail, 0, "Quota not applied in Wizard Folio") + + @freeze_time("2015-05-05") + def test_price_total_with_board_service(self): + """ + In booking engine when in availability results choose a room or several + and also choose a board service, the total price is calculated from price of the room, + number of nights, board service included price and number of guests + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + self.product_test1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + } + ) + self.board_service_test = self.env["pms.board.service"].create( + { + "name": "Test Board Service", + "default_code": "TBS", + } + ) + self.env["pms.board.service.line"].create( + { + "pms_board_service_id": self.board_service_test.id, + "product_id": self.product_test1.id, + "amount": 8, + "adults": True, + } + ) + self.board_service_room_type = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.test_room_type_double.id, + "pms_board_service_id": self.board_service_test.id, + "pms_property_id": self.pms_property1.id, + } + ) + # self.board_service_room_type.flush() + # ACT + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + + lines_availability_test = booking_engine.availability_results.filtered( + lambda r: r.room_type_id.id == self.test_room_type_double.id + ) + + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 1 + lines_availability_test[ + 0 + ].board_service_room_id = self.board_service_room_type.id + + self.test_room_type_double.list_price = 25 + + room_price = self.test_room_type_double.list_price + days = (checkout - checkin).days + board_service_price = self.board_service_test.amount + room_capacity = self.test_room_type_double.get_room_type_capacity( + self.pms_property1.id + ) + expected_price = room_price * days + ( + board_service_price * room_capacity * days + ) + + # ASSERT + self.assertEqual( + lines_availability_test[0].price_per_room, + expected_price, + "The total price calculation is wrong", + ) + + @freeze_time("2014-05-05") + def _test_board_service_discount(self): + """ + In booking engine when a discount is indicated it must be + applied correctly on both reservation lines and board services, + whether consumed after or before night + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + self.product_test1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + } + ) + self.board_service_test = self.env["pms.board.service"].create( + { + "name": "Test Board Service", + "default_code": "TBS", + } + ) + self.env["pms.board.service.line"].create( + { + "pms_board_service_id": self.board_service_test.id, + "product_id": self.product_test1.id, + "amount": 8, + "adults": True, + } + ) + self.board_service_room_type = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.test_room_type_double.id, + "pms_board_service_id": self.board_service_test.id, + "pms_property_id": self.pms_property1.id, + } + ) + discount = 15 + + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "discount": discount, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + + lines_availability_test = booking_engine.availability_results.filtered( + lambda r: r.room_type_id.id == self.test_room_type_double.id + ) + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 1 + lines_availability_test[ + 0 + ].board_service_room_id = self.board_service_room_type.id + + # ACT + booking_engine.create_folio() + + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + # ASSERT + for line in folio.service_ids.service_line_ids: + if line.is_board_service: + self.assertEqual( + line.discount, + discount * 100, + "The discount is not correctly established", + ) + + def test_check_folio_when_change_selection(self): + """ + Check, when creating a folio from booking engine, + if a room type is chosen and then deleted that selection + isn`t registered on the folio and is properly unselected + """ + # ARRANGE + # CREATION OF ROOM TYPE (WITH ROOM TYPE CLASS) + self.partner_id2 = self.env["res.partner"].create( + { + "name": "Brais", + "mobile": "654665553", + "email": "braistest@example.com", + } + ) + self.test_room_type_triple = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Triple Test", + "default_code": "TRP_Test", + "class_id": self.room_type_class1.id, + "list_price": 60.0, + } + ) + + # pms.room + self.test_room1_triple = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "Triple 301 test", + "room_type_id": self.test_room_type_triple.id, + "capacity": 3, + } + ) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id2.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + + lines_availability_test_double = booking_engine.availability_results.filtered( + lambda r: r.room_type_id.id == self.test_room_type_double.id + ) + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_double.id), + ("value", "=", 1), + ] + ) + lines_availability_test_double[0].num_rooms_selected = value + lines_availability_test_double[0].value_num_rooms_selected = 1 + + lines_availability_test_double[0].value_num_rooms_selected = 0 + + lines_availability_test_triple = booking_engine.availability_results.filtered( + lambda r: r.room_type_id.id == self.test_room_type_triple.id + ) + value_triple = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", self.test_room_type_triple.id), + ("value", "=", 1), + ] + ) + lines_availability_test_triple[0].num_rooms_selected = value_triple + lines_availability_test_triple[0].value_num_rooms_selected = 1 + + # ACT + booking_engine.create_folio() + + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id2.id)]) + # ASSERT + self.assertEqual( + len(folio.reservation_ids), + 1, + "Reservations of folio are incorrect", + ) + + def _test_adding_board_services_are_saved_on_lines(self): + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.pricelist1.id, + "pms_property_id": self.pms_property1.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + booking_engine.availability_results.filtered( + lambda r: r.room_type_id.id == self.test_room_type_double.id + ) + self.assertTrue(False) diff --git a/pms/tests/test_pms_checkin_partner.py b/pms/tests/test_pms_checkin_partner.py new file mode 100644 index 0000000000..d7ace36944 --- /dev/null +++ b/pms/tests/test_pms_checkin_partner.py @@ -0,0 +1,1662 @@ +import datetime +import logging + +from dateutil.relativedelta import relativedelta +from freezegun import freeze_time + +from odoo import fields +from odoo.exceptions import ValidationError + +from .common import TestPms + +_logger = logging.getLogger(__name__) + + +class TestPmsCheckinPartner(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + today = datetime.date(2012, 1, 14) + cls.room_type1 = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Triple", + "default_code": "TRP", + "class_id": cls.room_type_class1.id, + } + ) + cls.room1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Triple 101", + "room_type_id": cls.room_type1.id, + "capacity": 3, + } + ) + cls.room1_2 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Triple 111", + "room_type_id": cls.room_type1.id, + "capacity": 3, + } + ) + cls.room1_3 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Triple 222", + "room_type_id": cls.room_type1.id, + "capacity": 3, + } + ) + + cls.host1 = cls.env["res.partner"].create( + { + "name": "Miguel", + "email": "miguel@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + cls.id_category = cls.env["res.partner.id_category"].search( + [("code", "=", "D")] + ) + if not cls.id_category: + cls.id_category = cls.env["res.partner.id_category"].create( + {"name": "DNI", "code": "D"} + ) + cls.env["res.partner.id_number"].create( + { + "category_id": cls.id_category.id, + "name": "30065089H", + "valid_from": today, + "partner_id": cls.host1.id, + } + ) + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + reservation_vals = { + "checkin": today, + "checkout": today + datetime.timedelta(days=3), + "room_type_id": cls.room_type1.id, + "partner_id": cls.host1.id, + "adults": 3, + "pms_property_id": cls.pms_property1.id, + "sale_channel_origin_id": cls.sale_channel_direct1.id, + } + cls.reservation_1 = cls.env["pms.reservation"].create(reservation_vals) + cls.checkin1 = cls.env["pms.checkin.partner"].create( + { + "partner_id": cls.host1.id, + "reservation_id": cls.reservation_1.id, + } + ) + + def test_auto_create_checkins(self): + """ + Check that as many checkin_partners are created as there + adults on the reservation + + Reservation has three adults + """ + + # ACTION + checkins_count = len(self.reservation_1.checkin_partner_ids) + # ASSERT + self.assertEqual( + checkins_count, + 3, + "the automatic partner checkin was not created successful", + ) + + @freeze_time("2012-01-14") + def test_auto_unlink_checkins(self): + # ACTION + host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "mobile": "654667733", + "email": "carlos@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "85564627G", + "valid_from": datetime.date.today(), + "partner_id": host2.id, + } + ) + self.reservation_1.checkin_partner_ids = [ + ( + 0, + False, + { + "partner_id": host2.id, + }, + ) + ] + + checkins_count = len(self.reservation_1.checkin_partner_ids) + + # ASSERT + self.assertEqual( + checkins_count, + 3, + "the automatic partner checkin was not updated successful", + ) + + def test_onboard_checkin(self): + """ + Check that the reservation cannot be onboard because + checkin_partner data are incomplete and not have onboard status + """ + + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="Reservation state cannot be 'onboard'" + ): + self.reservation_1.state = "onboard" + + @freeze_time("2012-01-14") + def test_onboard_reservation(self): + """ + Check that reservation state is onboard as the checkin day is + today and checkin_partners data are complete + """ + # ACT + self.checkin1.action_on_board() + + # ASSERT + self.assertEqual( + self.reservation_1.state, + "onboard", + "the reservation checkin was not successful", + ) + + @freeze_time("2012-01-14") + def test_premature_checkin(self): + """ + Check that cannot change checkin_partner state to onboard if + it's not yet checkin day + """ + + # ARRANGE + self.reservation_1.write( + { + "checkin": datetime.date.today() + datetime.timedelta(days=1), + } + ) + # ACT & ASSERT + with self.assertRaises(ValidationError, msg="Cannot do checkin onboard"): + self.checkin1.action_on_board() + + @freeze_time("2012-01-14") + def test_late_checkin_on_checkout_day(self): + """ + Check that allowed register checkin arrival the next day + even if it is the same day of checkout + """ + + # ARRANGE + self.reservation_1.write( + { + "checkin": datetime.date.today() + datetime.timedelta(days=-1), + "checkout": datetime.date.today(), + } + ) + + # ACT + self.checkin1.action_on_board() + + # ASSERT + self.assertEqual( + self.checkin1.arrival, + fields.datetime.now(), + """The system did not allow to check in the next + day because it was the same day of checkout""", + ) + + @freeze_time("2012-01-13") + def test_late_checkin(self): + """ + When host arrives late anad has already passed the checkin day, + the arrival date is updated up to that time. + + In this case checkin day was 2012-01-14 and the host arrived a day later + so the arrival date is updated to that time + + """ + + # ARRANGE + self.reservation_1.write( + { + "checkin": datetime.date.today(), + } + ) + + # ACT + self.checkin1.action_on_board() + + # ASSERT + self.assertEqual( + self.checkin1.arrival, + fields.datetime.now(), + "the late checkin has problems", + ) + + @freeze_time("2012-01-14") + def test_too_many_people_checkin(self): + """ + Reservation cannot have more checkin_partners than adults who have + Reservation has three adults and cannot have four checkin_partner + """ + + # ARRANGE + host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "mobile": "654667733", + "email": "carlos@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "95876871Z", + "valid_from": datetime.date.today(), + "partner_id": host2.id, + } + ) + host3 = self.env["res.partner"].create( + { + "name": "Enmanuel", + "mobile": "654667733", + "email": "enmanuel@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "58261664L", + "valid_from": datetime.date.today(), + "partner_id": host3.id, + } + ) + host4 = self.env["res.partner"].create( + { + "name": "Enrique", + "mobile": "654667733", + "email": "enrique@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "61645604S", + "valid_from": datetime.date.today(), + "partner_id": host4.id, + } + ) + self.env["pms.checkin.partner"].create( + { + "partner_id": host2.id, + "reservation_id": self.reservation_1.id, + } + ) + self.env["pms.checkin.partner"].create( + { + "partner_id": host3.id, + "reservation_id": self.reservation_1.id, + } + ) + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Reservation cannot have more checkin_partner than adults who have", + ): + self.reservation_1.write( + { + "checkin_partner_ids": [ + ( + 0, + 0, + { + "partner_id": host4.id, + }, + ) + ] + } + ) + + @freeze_time("2012-01-14") + def test_count_pending_arrival_persons(self): + """ + After making onboard of two of the three checkin_partners, + one must remain pending arrival, that is a ratio of two thirds + """ + + # ARRANGE + self.host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "mobile": "654667733", + "email": "carlos@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "63073204M", + "valid_from": datetime.date.today(), + "partner_id": self.host2.id, + } + ) + self.host3 = self.env["res.partner"].create( + { + "name": "Enmanuel", + "mobile": "654667733", + "email": "enmanuel@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "70699468K", + "valid_from": datetime.date.today(), + "partner_id": self.host3.id, + } + ) + + self.checkin2 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host2.id, + "reservation_id": self.reservation_1.id, + } + ) + self.checkin3 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host3.id, + "reservation_id": self.reservation_1.id, + } + ) + + # ACT + self.checkin1.action_on_board() + self.checkin2.action_on_board() + + # ASSERT + self.assertEqual( + self.reservation_1.count_pending_arrival, + 1, + "Fail the count pending arrival on reservation", + ) + self.assertEqual( + self.reservation_1.checkins_ratio, + int(2 * 100 / 3), + "Fail the checkins ratio on reservation", + ) + + def test_complete_checkin_data(self): + """ + Reservation for three adults in a first place has three checkin_partners + pending data. Check that there decrease once their data are entered. + + Reservation has three adults, after entering data of two of them, + check that only one remains to be checked and the ratio of data entered + from checkin_partners is two thirds + """ + + # ARRANGE + self.host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "mobile": "654667733", + "email": "carlos@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "12650631X", + "valid_from": datetime.date.today(), + "partner_id": self.host2.id, + } + ) + # ACT + + self.checkin2 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host2.id, + "reservation_id": self.reservation_1.id, + } + ) + pending_checkin_data = self.reservation_1.pending_checkin_data + ratio_checkin_data = self.reservation_1.ratio_checkin_data + # ASSERT + self.assertEqual( + pending_checkin_data, + 1, + "Fail the count pending checkin data on reservation", + ) + self.assertEqual( + ratio_checkin_data, + int(2 * 100 / 3), + "Fail the checkins data ratio on reservation", + ) + + @freeze_time("2012-01-14") + def test_auto_arrival_delayed(self): + """ + The state of reservation 'arrival_delayed' happen when the checkin day + has already passed and the resrvation had not yet changed its state to onboard. + + The date that was previously set was 2012-01-14, + it was advanced two days (to 2012-01-16). + There are three reservations with checkin day on 2012-01-15, + after invoking the method auto_arrival_delayed + those reservation change their state to 'auto_arrival_delayed' + """ + + # ARRANGE + self.host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "mobile": "654667733", + "email": "carlos@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "61369791H", + "valid_from": datetime.date.today(), + "partner_id": self.host2.id, + } + ) + self.host3 = self.env["res.partner"].create( + { + "name": "Enmanuel", + "mobile": "654667733", + "email": "enmanuel@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "53563260D", + "valid_from": datetime.date.today(), + "partner_id": self.host3.id, + } + ) + self.host4 = self.env["res.partner"].create( + { + "name": "Enrique", + "mobile": "654667733", + "email": "enrique@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "63742138F", + "valid_from": datetime.date.today(), + "partner_id": self.host4.id, + } + ) + self.reservation_1.write( + { + "checkin": datetime.date.today() + datetime.timedelta(days=1), + "checkout": datetime.date.today() + datetime.timedelta(days=6), + "adults": 1, + } + ) + reservation2_vals = { + "checkin": datetime.date.today() + datetime.timedelta(days=1), + "checkout": datetime.date.today() + datetime.timedelta(days=6), + "adults": 1, + "room_type_id": self.room_type1.id, + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "folio_id": self.reservation_1.folio_id.id, + } + reservation3_vals = { + "checkin": datetime.date.today() + datetime.timedelta(days=1), + "checkout": datetime.date.today() + datetime.timedelta(days=6), + "adults": 1, + "room_type_id": self.room_type1.id, + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "folio_id": self.reservation_1.folio_id.id, + } + self.reservation_2 = self.env["pms.reservation"].create(reservation2_vals) + self.reservation_3 = self.env["pms.reservation"].create(reservation3_vals) + folio_1 = self.reservation_1.folio_id + PmsReservation = self.env["pms.reservation"] + + # ACTION + freezer = freeze_time("2012-01-16 10:00:00") + freezer.start() + PmsReservation.auto_arrival_delayed() + + arrival_delayed_reservations = folio_1.reservation_ids.filtered( + lambda r: r.state == "arrival_delayed" + ) + + # ASSERT + self.assertEqual( + len(arrival_delayed_reservations), + 3, + "Reservations not set like No Show", + ) + freezer.stop() + + @freeze_time("2012-01-14") + def test_auto_arrival_delayed_checkout(self): + """ + The state of reservation 'arrival_delayed' happen when the checkin day + has already passed and the reservation had not yet changed its state to onboard. + But, if checkout day is passed without checkout, the reservation pass to + departure delayed with a reservation note warning + + The date that was previously set was 2012-01-14, + it was advanced two days (to 2012-01-16). + There are three reservations with checkout day on 2012-01-15, + after invoking the method auto_arrival_delayed + those reservation change their state to 'departure_delayed' + """ + + # ARRANGE + self.host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "mobile": "654667733", + "email": "carlos@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "61369791H", + "valid_from": datetime.date.today(), + "partner_id": self.host2.id, + } + ) + self.host3 = self.env["res.partner"].create( + { + "name": "Enmanuel", + "mobile": "654667733", + "email": "enmanuel@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "53563260D", + "valid_from": datetime.date.today(), + "partner_id": self.host3.id, + } + ) + self.host4 = self.env["res.partner"].create( + { + "name": "Enrique", + "mobile": "654667733", + "email": "enrique@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "63742138F", + "valid_from": datetime.date.today(), + "partner_id": self.host4.id, + } + ) + self.reservation_1.write( + { + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=1), + "adults": 1, + } + ) + reservation2_vals = { + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=1), + "adults": 1, + "room_type_id": self.room_type1.id, + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "folio_id": self.reservation_1.folio_id.id, + } + reservation3_vals = { + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=1), + "adults": 1, + "room_type_id": self.room_type1.id, + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "folio_id": self.reservation_1.folio_id.id, + } + self.reservation_2 = self.env["pms.reservation"].create(reservation2_vals) + self.reservation_3 = self.env["pms.reservation"].create(reservation3_vals) + folio_1 = self.reservation_1.folio_id + PmsReservation = self.env["pms.reservation"] + + # ACTION + freezer = freeze_time("2012-01-16 10:00:00") + freezer.start() + PmsReservation.auto_arrival_delayed() + + departure_delayed_reservations = folio_1.reservation_ids.filtered( + lambda r: r.state == "arrival_delayed" + ) + # ASSERT + self.assertEqual( + len(departure_delayed_reservations), + 3, + "Reservations not set like No Show", + ) + freezer.stop() + + @freeze_time("2012-01-14") + def test_auto_departure_delayed(self): + """ + When it's checkout dat and the reservation + was in 'onboard' state, that state change to + 'departure_delayed' if the manual checkout wasn't performed. + + The date that was previously set was 2012-01-14, + it was advanced two days (to 2012-01-17). + Reservation1 has checkout day on 2012-01-17, + after invoking the method auto_departure_delayed + this reservation change their state to 'auto_departure_delayed' + """ + + # ARRANGE + self.reservation_1.write( + { + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=3), + "adults": 1, + } + ) + PmsReservation = self.env["pms.reservation"] + self.checkin1.action_on_board() + + # ACTION + freezer = freeze_time("2012-01-17 12:00:00") + freezer.start() + PmsReservation.auto_departure_delayed() + + freezer.stop() + # ASSERT + self.assertEqual( + self.reservation_1.state, + "departure_delayed", + "Reservations not set like Departure delayed", + ) + + # REVIEW: Redesing constrains mobile&mail control + # @freeze_time("2010-12-10") + # def test_not_valid_emails(self): + # # TEST CASES + # # Check that the email format is incorrect + + # # ARRANGE + # reservation = self.env["pms.reservation"].create( + # { + # "checkin": datetime.date.today(), + # "checkout": datetime.date.today() + datetime.timedelta(days=3), + # "room_type_id": self.room_type1.id, + # "partner_id": self.env.ref("base.res_partner_12").id, + # "adults": 3, + # "pms_property_id": self.pms_property1.id, + # } + # ) + # test_cases = [ + # "myemail", + # "myemail@", + # "myemail@", + # "myemail@.com", + # ".myemail", + # ".myemail@", + # ".myemail@.com" ".myemail@.com." "123myemail@aaa.com", + # ] + # for mail in test_cases: + # with self.subTest(i=mail): + # with self.assertRaises( + # ValidationError, msg="Email format is correct and shouldn't" + # ): + # reservation.write( + # { + # "checkin_partner_ids": [ + # ( + # 0, + # False, + # { + # "name": "Carlos", + # "email": mail, + # }, + # ) + # ] + # } + # ) + + # @freeze_time("2014-12-10") + # def test_valid_emails(self): + # # TEST CASES + # # Check that the email format is correct + + # # ARRANGE + # reservation = self.env["pms.reservation"].create( + # { + # "checkin": datetime.date.today(), + # "checkout": datetime.date.today() + datetime.timedelta(days=4), + # "room_type_id": self.room_type1.id, + # "partner_id": self.env.ref("base.res_partner_12").id, + # "adults": 3, + # "pms_property_id": self.pms_property1.id, + # } + # ) + # test_cases = [ + # "hello@commitsun.com", + # "hi.welcome@commitsun.com", + # "hi.welcome@dev.commitsun.com", + # "hi.welcome@dev-commitsun.com", + # "john.doe@xxx.yyy.zzz", + # ] + # for mail in test_cases: + # with self.subTest(i=mail): + # guest = self.env["pms.checkin.partner"].create( + # { + # "name": "Carlos", + # "email": mail, + # "reservation_id": reservation.id, + # } + # ) + # self.assertEqual( + # mail, + # guest.email, + # ) + # guest.unlink() + + # @freeze_time("2016-12-10") + # def test_not_valid_phone(self): + # # TEST CASES + # # Check that the phone format is incorrect + + # # ARRANGE + # reservation = self.env["pms.reservation"].create( + # { + # "checkin": datetime.date.today(), + # "checkout": datetime.date.today() + datetime.timedelta(days=1), + # "room_type_id": self.room_type1.id, + # "partner_id": self.env.ref("base.res_partner_12").id, + # "adults": 3, + # "pms_property_id": self.pms_property1.id, + # } + # ) + # test_cases = [ + # "phone", + # "123456789123", + # "123.456.789", + # "123", + # "123123", + # ] + # for phone in test_cases: + # with self.subTest(i=phone): + # with self.assertRaises( + # ValidationError, msg="Phone format is correct and shouldn't" + # ): + # self.env["pms.checkin.partner"].create( + # { + # "name": "Carlos", + # "mobile": phone, + # "reservation_id": reservation.id, + # } + # ) + + # @freeze_time("2018-12-10") + # def test_valid_phones(self): + # # TEST CASES + # # Check that the phone format is correct + + # # ARRANGE + # reservation = self.env["pms.reservation"].create( + # { + # "checkin": datetime.date.today(), + # "checkout": datetime.date.today() + datetime.timedelta(days=5), + # "room_type_id": self.room_type1.id, + # "partner_id": self.env.ref("base.res_partner_12").id, + # "adults": 3, + # "pms_property_id": self.pms_property1.id, + # } + # ) + # test_cases = [ + # "981 981 981", + # "981981981", + # "981 98 98 98", + # ] + # for mobile in test_cases: + # with self.subTest(i=mobile): + # guest = self.env["pms.checkin.partner"].create( + # { + # "name": "Carlos", + # "mobile": mobile, + # "reservation_id": reservation.id, + # } + # ) + # self.assertEqual( + # mobile, + # guest.mobile, + # ) + + def test_complete_checkin_data_with_partner_data(self): + """ + When a partner is asociated with a checkin, checkin data + will be equal to the partner data + + Host1: + "email": "miguel@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + + Checkin1: + "partner_id": host1.id + + So after this: + Checkin1: + "email": "miguel@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + """ + # ARRANGE + partner_data = [self.host1.birthdate_date, self.host1.email, self.host1.gender] + checkin_data = [ + self.checkin1.birthdate_date, + self.checkin1.email, + self.checkin1.gender, + ] + + # ASSERT + for i in [0, 1, 2]: + self.assertEqual( + partner_data[i], + checkin_data[i], + "Checkin data must be the same as partner data ", + ) + + def test_create_partner_when_checkin_has_enought_data(self): + """ + Check that partner is created when the necessary minimum data is entered + into checkin_partner data + """ + # ACT & ASSERT + checkin = self.env["pms.checkin.partner"].create( + { + "firstname": "Pepe", + "lastname": "Paz", + "document_type": self.id_category.id, + "document_number": "77156490T", + "reservation_id": self.reservation_1.id, + } + ) + + # ASSERT + self.assertTrue( + checkin.partner_id, + "Partner should have been created and associated with the checkin", + ) + + def test_not_create_partner_checkin_hasnt_enought_data(self): + """ + Check that partner is not created when the necessary minimum data isn't entered + into checkin_partner data, in this case document_id and document_number + """ + # ACT & ASSERT + checkin = self.env["pms.checkin.partner"].create( + { + "firstname": "Pepe", + "lastname": "Paz", + "email": "pepepaz@gmail.com", + "mobile": "666777777", + "reservation_id": self.reservation_1.id, + } + ) + + # ASSERT + self.assertFalse( + checkin.partner_id, + "Partner mustn't have been created and associated with the checkin", + ) + + def test_add_partner_data_from_checkin(self): + """ + If the checkin_partner has some data that the partner doesn't have, + these are saved in the partner + + In this case, host1 hasn't mobile but the checkin_partner associated with it does, + so the mobile of checkin_partner is added to the partner data + + Note that if the mobile is entered before partnee was associated, this or other fields + are overwritten by the partner's fields. In this case it is entered once the partner has + already been associated + """ + # ARRANGE + self.checkin1.mobile = "666777888" + # ASSERT + self.assertTrue(self.host1.mobile, "Partner mobile must be added") + + def test_partner_id_numbers_created_from_checkin(self): + """ + Some of the required data of the checkin_partner to create the partner are document_type + and document_number, with them an id_number is created associated with the partner that + has just been created. + In this test it is verified that this document has been created correctly + """ + # ACT & ARRANGE + checkin = self.env["pms.checkin.partner"].create( + { + "firstname": "Pepe", + "lastname": "Paz", + "document_type": self.id_category.id, + "document_number": "77156490T", + "reservation_id": self.reservation_1.id, + } + ) + + checkin.flush() + + # ASSERT + self.assertTrue( + checkin.partner_id.id_numbers, + "Partner id_number should have been created and hasn't been", + ) + + def _test_partner_not_modified_when_checkin_modified(self): + """ + If a partner is associated with a checkin + and some of their data is modified in the checkin, + they will not be modified in the partner + """ + # ARRANGE + self.checkin1.email = "prueba@gmail.com" + + # ASSERT + self.assertNotEqual( + self.host1.email, + self.checkin1.email, + "Checkin partner email and partner email shouldn't match", + ) + + def test_partner_modified_previous_checkin_not_modified(self): + """ + If a partner modifies any of its fields, these change mustn't be reflected + in the previous checkins associated with it + """ + # ARRANGE + self.checkin1.flush() + self.host1.gender = "female" + # ASSERT + self.assertNotEqual( + self.host1.gender, + self.checkin1.gender, + "Checkin partner gender and partner gender shouldn't match", + ) + + def test_add_partner_if_exists_from_checkin(self): + """ + Check when a document_type and document_number are entered in a checkin if this + document already existes and is associated with a partner, this partner will be + associated with the checkin + """ + # ACT + host = self.env["res.partner"].create( + { + "name": "Ricardo", + "mobile": "666555666", + "email": "ricardo@example.com", + "birthdate_date": "1995-11-14", + "gender": "male", + } + ) + + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "55562998N", + "partner_id": host.id, + } + ) + + # ARRANGE + checkin = self.env["pms.checkin.partner"].create( + { + "document_type": self.id_category.id, + "document_number": "55562998N", + "reservation_id": self.reservation_1.id, + } + ) + + # ASSERT + self.assertEqual( + checkin.partner_id.id, + host.id, + "Checkin partner_id must be the same as the one who has that document", + ) + + def test_is_possible_customer_by_email(self): + """ + It is checked that the field possible_existing_customer_ids + exists in a checkin partner with an email from a res.partner saved + in the DB. + ---------------- + A res.partner is created with the name and email fields. A checkin partner + is created by adding the same email as the res.partner. Then it is + checked that some possible_existing_customer_ids exists. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "name": "Courtney Campbell", + "email": "courtney@example.com", + } + ) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type1.id, + "pms_property_id": self.pms_property1.id, + "partner_name": partner.name, + "email": partner.email, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ACT + checkin = self.env["pms.checkin.partner"].create( + { + "name": partner.name, + "email": partner.email, + "reservation_id": reservation.id, + } + ) + # ASSERT + self.assertTrue( + checkin.possible_existing_customer_ids, + "No customer found with this email", + ) + + def test_is_possible_customer_by_mobile(self): + """ + It is checked that the field possible_existing_customer_ids + exists in a checkin partner with a mobile from a res.partner saved + in the DB. + ---------------- + A res.partner is created with the name and email fields. A checkin partner + is created by adding the same mobile as the res.partner. Then it is + checked that some possible_existing_customer_ids exists. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "name": "Ledicia Sandoval", + "mobile": "615369231", + } + ) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type1.id, + "pms_property_id": self.pms_property1.id, + "partner_name": partner.name, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ACT + checkin = self.env["pms.checkin.partner"].create( + { + "name": partner.name, + "mobile": partner.mobile, + "reservation_id": reservation.id, + } + ) + # ASSERT + self.assertTrue( + checkin.possible_existing_customer_ids, + "No customer found with this mobile", + ) + + def test_add_possible_customer(self): + """ + Check that a partner was correctly added to the checkin partner + after launching the add_partner() method of the several partners wizard + --------------- + A res.partner is created with name, email and mobile. A checkin partner is + created with the email field equal to that of the res.partner created before. + The wizard is created with the checkin partner id and the partner added to the + possible_existing_customer_ids field. The add_partner method of the wizard + is launched and it is checked that the partner was correctly added to the + checkin partner. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "name": "Serafín Rivas", + "email": "serafin@example.com", + "mobile": "60595595", + } + ) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type1.id, + "pms_property_id": self.pms_property1.id, + "partner_name": partner.name, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + checkin = self.env["pms.checkin.partner"].create( + { + "name": partner.name, + "email": partner.email, + "reservation_id": reservation.id, + } + ) + + several_partners_wizard = self.env["pms.several.partners.wizard"].create( + { + "checkin_partner_id": checkin.id, + "possible_existing_customer_ids": [(6, 0, [partner.id])], + } + ) + # ACT + several_partners_wizard.add_partner() + # ASSERT + self.assertEqual( + checkin.partner_id.id, + partner.id, + "The partner was not added to the checkin partner ", + ) + + def test_not_add_several_possibles_customers(self): + """ + Check that multiple partners cannot be added to a checkin partner + from the several partners wizard. + --------------- + Two res.partner are created with name, email and mobile. A checkin partner is + created with the email field equal to that of the partner1 created before. + The wizard is created with the checkin partner id and the two partners added to the + possible_existing_customer_ids field. The add_partner method of the wizard + is launched and it is verified that a Validation_Error was raised. + """ + # ARRANGE + partner1 = self.env["res.partner"].create( + { + "name": "Serafín Rivas", + "email": "serafin@example.com", + "mobile": "60595595", + } + ) + partner2 = self.env["res.partner"].create( + { + "name": "Simon", + "mobile": "654667733", + "email": "simon@example.com", + } + ) + + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type1.id, + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + checkin = self.env["pms.checkin.partner"].create( + { + "name": partner1.name, + "email": partner1.email, + "reservation_id": reservation.id, + } + ) + + several_partners_wizard = self.env["pms.several.partners.wizard"].create( + { + "checkin_partner_id": checkin.id, + "possible_existing_customer_ids": [(6, 0, [partner1.id, partner2.id])], + } + ) + + # ACT AND ASSERT + with self.assertRaises( + ValidationError, + msg="Two partners cannot be added to the checkin partner", + ): + several_partners_wizard.add_partner() + + def test_not_add_any_possibles_customers(self): + """ + Check that the possible_existing_customer_ids field of the several + partners wizard can be left empty and then launch the add_partner() + method of this wizard to add a partner in checkin_partner. + --------------- + A checkin_partner is created. The wizard is created without the + possible_existing_customer_ids field. The add_partner method of + the wizard is launched and it is verified that a Validation_Error + was raised. + """ + + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type1.id, + "pms_property_id": self.pms_property1.id, + "partner_name": "Rosa Costa", + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + checkin = self.env["pms.checkin.partner"].create( + {"name": "Rosa Costa", "reservation_id": reservation.id} + ) + + several_partners_wizard = self.env["pms.several.partners.wizard"].create( + { + "checkin_partner_id": checkin.id, + } + ) + + # ACT AND ASSERT + with self.assertRaises( + ValidationError, + msg="A partner can be added to the checkin partner", + ): + several_partners_wizard.add_partner() + + def test_calculate_dni_expedition_date_from_validity_date_age_lt_30(self): + """ + Check that the calculate_doc_type_expedition_date_from_validity_date() + method calculates correctly the expedition_date of an id category DNI + when the age is less than 30. + ------------- + We launch the method calculate_doc_type_expedition_date_from_validity_date + with the parameters doc_type_id DNI, birthdate calculated so that the age + is = 20 years old and document_date = today + 1 year. The expected + expedition date has to be doc_date - 5 years + """ + doc_date = fields.date.today() + relativedelta(years=1) + doc_date_str = str(doc_date) + + # age=20 years old + birthdate = fields.date.today() - relativedelta(years=20) + birthdate_str = str(birthdate) + + # expected_expedition_date = doc_date - 5 years + expected_exp_date = doc_date - relativedelta(years=5) + expedition_date = ( + self.checkin1.calculate_doc_type_expedition_date_from_validity_date( + self.id_category, doc_date_str, birthdate_str + ) + ) + date_expedition_date = datetime.date( + year=expedition_date.year, + month=expedition_date.month, + day=expedition_date.day, + ) + self.assertEqual( + date_expedition_date, + expected_exp_date, + "Expedition date doesn't correspond with expected expedition date", + ) + + def test_calculate_dni_expedition_date_from_validity_date_age_gt_30(self): + """ + Check that the calculate_doc_type_expedition_date_from_validity_date() + method calculates correctly the expedition_date of an id category DNI + when the age is greater than 30. + ------------- + We launch the method calculate_doc_type_expedition_date_from_validity_date + with the parameters doc_type_id DNI, birthdate calculated so that the age + is = 40 years old and document_date = today + 1 year. The expected + expedition date has to be doc_date - 10 years + """ + doc_date = fields.date.today() + relativedelta(years=1) + doc_date_str = str(doc_date) + + # age=40 years old + birthdate = fields.date.today() - relativedelta(years=40) + birthdate_str = str(birthdate) + + # expected_expedition_date = doc_date - 10 years + expected_exp_date = doc_date - relativedelta(years=10) + expedition_date = ( + self.checkin1.calculate_doc_type_expedition_date_from_validity_date( + self.id_category, doc_date_str, birthdate_str + ) + ) + date_expedition_date = datetime.date( + year=expedition_date.year, + month=expedition_date.month, + day=expedition_date.day, + ) + self.assertEqual( + date_expedition_date, + expected_exp_date, + "Expedition date doesn't correspond with expected expedition date", + ) + + def test_calculate_passport_expedition_date_from_validity_date_age_lt_30(self): + """ + Check that the calculate_doc_type_expedition_date_from_validity_date() + method calculates correctly the expedition_date of an id category Passport + when the age is less than 30. + ------------- + We launch the method calculate_doc_type_expedition_date_from_validity_date + with the parameters doc_type_id Passport, birthdate calculated so that the age + is = 20 years old and document_date = today + 1 year. The expected + expedition date has to be doc_date - 5 years + """ + doc_date = fields.date.today() + relativedelta(years=1) + doc_date_str = str(doc_date) + + # age=20 years old + birthdate = fields.date.today() - relativedelta(years=20) + birthdate_str = str(birthdate) + + # expected_expedition_date = doc_date - 5 years + expected_exp_date = doc_date - relativedelta(years=5) + expedition_date = ( + self.checkin1.calculate_doc_type_expedition_date_from_validity_date( + self.id_category, doc_date_str, birthdate_str + ) + ) + date_expedition_date = datetime.date( + year=expedition_date.year, + month=expedition_date.month, + day=expedition_date.day, + ) + self.assertEqual( + date_expedition_date, + expected_exp_date, + "Expedition date doesn't correspond with expected expedition date", + ) + + def test_calculate_passport_expedition_date_from_validity_date_age_gt_30(self): + """ + Check that the calculate_doc_type_expedition_date_from_validity_date() + method calculates correctly the expedition_date of an id category Passport + when the age is greater than 30. + ------------- + We launch the method calculate_doc_type_expedition_date_from_validity_date + with the parameters doc_type_id Passport, birthdate calculated so that the age + is = 40 years old and document_date = today + 1 year. The expected + expedition date has to be doc_date - 10 years + """ + doc_type_id = self.env["res.partner.id_category"].search([("code", "=", "P")]) + doc_date = fields.date.today() + relativedelta(years=1) + doc_date_str = str(doc_date) + + # age=40 years old + birthdate = fields.date.today() - relativedelta(years=40) + birthdate_str = str(birthdate) + + # expected_expedition_date = doc_date - 10 years + expected_exp_date = doc_date - relativedelta(years=10) + expedition_date = ( + self.checkin1.calculate_doc_type_expedition_date_from_validity_date( + doc_type_id, doc_date_str, birthdate_str + ) + ) + date_expedition_date = datetime.date( + year=expedition_date.year, + month=expedition_date.month, + day=expedition_date.day, + ) + self.assertEqual( + date_expedition_date, + expected_exp_date, + "Expedition date doesn't correspond with expected expedition date", + ) + + def test_calculate_drive_license_expedition_date_from_validity_date_age_lt_70(self): + """ + Check that the calculate_doc_type_expedition_date_from_validity_date() + method calculates correctly the expedition_date of an id category Driving + License when the age is lesser than 70. + ------------- + We launch the method calculate_doc_type_expedition_date_from_validity_date + with the parameters doc_type_id DNI, birthdate calculated so that the age + is = 40 years old and document_date = today + 1 year. The expected + expedition date has to be doc_date - 10 years + """ + doc_type_id = self.env["res.partner.id_category"].search([("code", "=", "C")]) + doc_date = fields.date.today() + relativedelta(years=1) + doc_date_str = str(doc_date) + + # age=40 years old + birthdate = fields.date.today() - relativedelta(years=40) + birthdate_str = str(birthdate) + + # expected_expedition_date = doc_date - 10 years + expected_exp_date = doc_date - relativedelta(years=10) + expedition_date = ( + self.checkin1.calculate_doc_type_expedition_date_from_validity_date( + doc_type_id, doc_date_str, birthdate_str + ) + ) + date_expedition_date = datetime.date( + year=expedition_date.year, + month=expedition_date.month, + day=expedition_date.day, + ) + self.assertEqual( + date_expedition_date, + expected_exp_date, + "Expedition date doesn't correspond with expected expedition date", + ) + + def test_calculate_expedition_date(self): + """ + Check that if the value of the doc_date is less than today, + the method calculate_doc_type_expedition_date_from_validity_date + returns the value of the doc_date as expedition_date. + ----------- + We launch the method calculate_doc_type_expedition_date_from_validity_date + with the parameters doc_type_id DNI, birthdate calculated so that the age + is = 20 years old and document_date = today - 1 year. The expected + expedition date has to be the value of doc_date. + """ + doc_type_id = self.env["res.partner.id_category"].search([("code", "=", "D")]) + doc_date = fields.date.today() - relativedelta(years=1) + doc_date_str = str(doc_date) + birthdate = fields.date.today() - relativedelta(years=20) + birthdate_str = str(birthdate) + expedition_date = ( + self.checkin1.calculate_doc_type_expedition_date_from_validity_date( + doc_type_id, doc_date_str, birthdate_str + ) + ) + date_expedition_date = datetime.date( + year=expedition_date.year, + month=expedition_date.month, + day=expedition_date.day, + ) + self.assertEqual( + date_expedition_date, + doc_date, + "Expedition date doesn't correspond with expected expedition date", + ) + + def test_save_checkin_from_portal(self): + """ + Check by subtesting that a checkin partner is saved correctly + with the _save_data_from_portal() method. + --------- + A reservation is created with an adult, and it will create a checkin partner. + A dictionary is created with the values to be saved and with the key 'id' + equal to the id of the checkin_partner created when the reservation was + created. We launch the _save_data_from_portal() method, passing the created + dictionary as a parameter. Then it is verified that the value of each key + in the dictionary corresponds to the fields of the saved checkin_partner. + """ + self.reservation = self.env["pms.reservation"].create( + { + "checkin": datetime.date.today() + datetime.timedelta(days=10), + "checkout": datetime.date.today() + datetime.timedelta(days=13), + "room_type_id": self.room_type1.id, + "partner_id": self.host1.id, + "adults": 1, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + checkin_partner = self.reservation.checkin_partner_ids[0] + checkin_partner_vals = { + "checkin_partner": checkin_partner, + "id": checkin_partner.id, + "firstname": "Serafín", + "lastname": "Rivas", + "lastname2": "Gonzalez", + "document_type": self.id_category, + "document_number": "18038946T", + "document_expedition_date": "07/10/2010", + "birthdate_date": "05/10/1983", + "mobile": "60595595", + "email": "serafin@example.com", + "gender": "male", + "nationality_id": 1, + "residence_state_id": 1, + } + checkin_partner._save_data_from_portal(checkin_partner_vals) + checkin_partner_vals.update( + { + "birthdate_date": datetime.date(1983, 10, 5), + "document_expedition_date": datetime.date(2010, 10, 7), + "nationality_id": self.env["res.country"].search([("id", "=", 1)]), + "residence_state_id": self.env["res.country.state"].browse(1), + "document_type": self.id_category, + } + ) + for key in checkin_partner_vals: + with self.subTest(k=key): + self.assertEqual( + self.reservation.checkin_partner_ids[0][key], + checkin_partner_vals[key], + "The value of " + key + " is not correctly established", + ) + + def test_compute_partner_fields(self): + """ + Check that the computes of the checkin_partner fields related to your partner correctly + add these fields to the checkin_partner. + --------------------------------------- + A reservation is created with an adult (checkin_partner) ql which is saved in the + checkin_partner_id variable, a partner is also created with all the fields that are + related to the checkin_partner fields. The partner is added to the partner_id field + of the checkin_partner and, through subtests, it is verified that the fields of the + partner and the associated checkin_partner match. + """ + self.reservation = self.env["pms.reservation"].create( + { + "checkin": datetime.date.today() + datetime.timedelta(days=1), + "checkout": datetime.date.today() + datetime.timedelta(days=2), + "room_type_id": self.room_type1.id, + "partner_id": self.host1.id, + "adults": 1, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + checkin_partner = self.reservation.checkin_partner_ids[0] + nationality = self.env["res.country"].search( + [ + ("state_ids", "!=", False), + ], + limit=1, + ) + state = nationality.state_ids[0] + partner_vals = { + "firstname": "Paz", + "lastname": "Valenzuela", + "lastname2": "Soto", + "email": "paz@example.com", + "birthdate_date": datetime.date(1980, 10, 5), + "gender": "female", + "mobile": "666555444", + "phone": "123456789", + "nationality_id": nationality.id, + "residence_street": "Calle 123", + "residence_street2": "Avda. Constitución 123", + "residence_zip": "15700", + "residence_city": "City Residence", + "residence_country_id": nationality.id, + "residence_state_id": state.id, + # "pms_checkin_partner_ids": checkin_partner_id, + } + self.partner_id = self.env["res.partner"].create(partner_vals) + + partner_vals.update( + { + "nationality_id": nationality, + "residence_country_id": nationality, + "residence_state_id": state, + } + ) + + checkin_partner.partner_id = self.partner_id.id + for key in partner_vals: + if key != "pms_checkin_partner_ids": + with self.subTest(k=key): + self.assertEqual( + self.reservation.checkin_partner_ids[0][key], + self.partner_id[key], + "The value of " + key + " is not correctly established", + ) diff --git a/pms/tests/test_pms_folio.py b/pms/tests/test_pms_folio.py new file mode 100644 index 0000000000..f1a1df4d30 --- /dev/null +++ b/pms/tests/test_pms_folio.py @@ -0,0 +1,1476 @@ +import datetime + +from freezegun import freeze_time + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests import Form + +from .common import TestPms + + +class TestPmsFolio(TestPms): + + # SetUp and Common Scenarios methods + + @classmethod + def setUpClass(cls): + """ + - common + room_type_double with 2 rooms (double1 and double2) in pms_property1 + """ + super().setUpClass() + + # create room type + cls.room_type_double = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + "price": 25, + } + ) + # create room + cls.double1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 101", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + } + ) + + # create room + cls.double2 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 102", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + } + ) + # make current journals payable + journals = cls.env["account.journal"].search( + [ + ("type", "in", ["bank", "cash"]), + ] + ) + journals.allowed_pms_payments = True + + # create sale channel direct + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + + def create_sale_channel_scenario(self): + """ + Method to simplified scenario on sale channel tests: + - create a sale_channel1 like indirect + - create a agency1 like sale_channel1 agency + """ + PmsPartner = self.env["res.partner"] + PmsSaleChannel = self.env["pms.sale.channel"] + + self.sale_channel1 = PmsSaleChannel.create( + {"name": "saleChannel1", "channel_type": "indirect"} + ) + self.agency1 = PmsPartner.create( + { + "name": "partner1", + "is_agency": True, + "invoice_to_agency": "always", + "default_commission": 15, + "sale_channel_id": self.sale_channel1.id, + } + ) + + def create_configuration_accounting_scenario(self): + """ + Method to simplified scenario to payments and accounting: + # REVIEW: + - Use new property with odoo demo data company to avoid account configuration + - Emule SetUp with new property: + - create demo_room_type_double + - Create 2 rooms room_type_double + """ + self.pms_property_demo = self.env["pms.property"].create( + { + "name": "Property Based on Comapany Demo", + "company_id": self.env.ref("base.main_company").id, + "default_pricelist_id": self.env.ref("product.list0").id, + } + ) + # create room type + self.demo_room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property_demo.id], + "name": "Double Test", + "default_code": "Demo_DBL_Test", + "class_id": self.room_type_class1.id, + "price": 25, + } + ) + # create rooms + self.double1 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property_demo.id, + "name": "Double 101", + "room_type_id": self.demo_room_type_double.id, + "capacity": 2, + } + ) + self.double2 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property_demo.id, + "name": "Double 102", + "room_type_id": self.demo_room_type_double.id, + "capacity": 2, + } + ) + + # TestCases: Sale Channels + + def test_default_agency_commission(self): + """ + Check the total commission of a folio with agency based on the + reservation night price and the preconfigured commission in the agency. + ------- + Agency with 15% default commision, folio with one reservation + and 3 nights at 20$ by night (60$ total) + """ + # ARRANGE + self.create_sale_channel_scenario() + # ACT + folio1 = self.env["pms.folio"].create( + { + "agency_id": self.agency1.id, + "pms_property_id": self.pms_property1.id, + } + ) + + self.env["pms.reservation"].create( + { + "folio_id": folio1.id, + "room_type_id": self.room_type_double.id, + "reservation_line_ids": [ + ( + 0, + False, + { + "date": fields.date.today(), + "price": 20, + }, + ), + ( + 0, + False, + { + "date": fields.date.today() + datetime.timedelta(days=1), + "price": 20, + }, + ), + ( + 0, + False, + { + "date": fields.date.today() + datetime.timedelta(days=2), + "price": 20, + }, + ), + ], + } + ) + self.commission = 0 + for reservation in folio1.reservation_ids: + self.commission = ( + self.commission + + reservation.price_total * self.agency1.default_commission / 100 + ) + + # ASSERT + self.assertEqual( + self.commission, folio1.commission, "The folio compute commission is wrong" + ) + + def test_folio_commission(self): + """ + Check commission of a folio with several reservations that have commission + """ + # ARRANGE + self.create_sale_channel_scenario() + + # ACT + folio1 = self.env["pms.folio"].create( + { + "agency_id": self.agency1.id, + "pms_property_id": self.pms_property1.id, + } + ) + + self.env["pms.reservation"].create( + { + "folio_id": folio1.id, + "room_type_id": self.room_type_double.id, + "reservation_line_ids": [ + ( + 0, + False, + { + "date": fields.date.today(), + "price": 20, + }, + ), + ], + } + ) + self.env["pms.reservation"].create( + { + "folio_id": folio1.id, + "room_type_id": self.room_type_double.id, + "reservation_line_ids": [ + ( + 0, + False, + { + "date": fields.date.today(), + "price": 40, + }, + ), + ], + } + ) + + self.commission = 0 + for reservation in folio1.reservation_ids: + if reservation.commission_amount != 0: + self.commission = ( + self.commission + + reservation.price_total * self.agency1.default_commission / 100 + ) + self.folio_commission = folio1.commission + # ASSERT + self.assertEqual( + self.commission, + self.folio_commission, + "The folio compute commission is wrong", + ) + + def test_folio_commission_with_reservations_without_commission(self): + """ + Check commission of a folio with several reservations, + of which the last hasn't commission + + --- folio1: + -reservation1: commission 15% --> commission amount 3.00 + -reservation2: commission 0% --> commission amount 0.00 + + folio1 commission --> 3.00 + """ + # ARRANGE + self.create_sale_channel_scenario() + + # ACT + + folio1 = self.env["pms.folio"].create( + { + "agency_id": self.agency1.id, + "pms_property_id": self.pms_property1.id, + } + ) + + self.env["pms.reservation"].create( + { + "folio_id": folio1.id, + "room_type_id": self.room_type_double.id, + "reservation_line_ids": [ + ( + 0, + False, + { + "date": fields.date.today(), + "price": 20, + }, + ), + ], + } + ) + + self.env["pms.reservation"].create( + { + "folio_id": folio1.id, + "room_type_id": self.room_type_double.id, + "reservation_line_ids": [ + ( + 0, + False, + { + "date": fields.date.today(), + "price": 40, + }, + ), + ], + "commission_percent": 0, + } + ) + self.commission = 0 + for reservation in folio1.reservation_ids: + if reservation.commission_amount != 0: + self.commission = ( + self.commission + + reservation.price_total * self.agency1.default_commission / 100 + ) + self.folio_commission = folio1.commission + # ASSERT + self.assertEqual( + self.commission, + self.folio_commission, + "The folio compute commission is wrong", + ) + + def test_reservation_agency_without_partner(self): + """ + Check that a reservation / folio created with an agency + and without a partner will automatically take the partner. + ------- + Create the folio1 and the reservation1, only set agency_id, + and the partner_id should be the agency itself. + """ + # ARRANGE + self.create_sale_channel_scenario() + + # ACT + folio1 = self.env["pms.folio"].create( + { + "agency_id": self.agency1.id, + "pms_property_id": self.pms_property1.id, + } + ) + + reservation1 = self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "folio_id": folio1.id, + } + ) + + # ASSERT + self.assertEqual( + reservation1.agency_id, folio1.partner_id, "Agency has to be the partner" + ) + + # TestCases: Payments + @freeze_time("2000-02-02") + def test_full_pay_folio(self): + """ + After making the payment of a folio for the entire amount, + check that there is nothing pending. + ----- + We create a reservation (autocalculates the amounts) and + then make the payment using the do_payment method of the folio, + directly indicating the pending amount on the folio of the newly + created reservation + """ + # ARRANGE + self.create_configuration_accounting_scenario() + reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property_demo.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "partner_id": self.env.ref("base.res_partner_12").id, + "room_type_id": self.demo_room_type_double.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACTION + self.env["pms.folio"].do_payment( + journal=self.env["account.journal"].browse( + reservation1.folio_id.pms_property_id._get_payment_methods().ids[0] + ), + receivable_account=self.env["account.journal"] + .browse(reservation1.folio_id.pms_property_id._get_payment_methods().ids[0]) + .suspense_account_id, + user=self.env.user, + amount=reservation1.folio_id.pending_amount, + folio=reservation1.folio_id, + partner=reservation1.partner_id, + date=fields.date.today(), + ) + + # ASSERT + self.assertFalse( + reservation1.folio_id.pending_amount, + "The pending amount of a folio paid in full has not been zero", + ) + + @freeze_time("2000-02-02") + def test_partial_pay_folio(self): + """ + After making the payment of a folio for the partial amount, + We check that the pending amount is the one that corresponds to it. + ----- + We create a reservation (autocalculates the amounts) and + then make the payment using the do_payment method of the folio, + directly indicating the pending amount on the folio MINUS 1$ + of the newly created reservation + """ + # ARRANGE + self.create_configuration_accounting_scenario() + left_to_pay = 1 + reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property_demo.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "partner_id": self.env.ref("base.res_partner_12").id, + "room_type_id": self.demo_room_type_double.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACTION + self.env["pms.folio"].do_payment( + journal=self.env["account.journal"].browse( + reservation1.folio_id.pms_property_id._get_payment_methods().ids[0] + ), + receivable_account=self.env["account.journal"] + .browse(reservation1.folio_id.pms_property_id._get_payment_methods().ids[0]) + .suspense_account_id, + user=self.env.user, + amount=reservation1.folio_id.pending_amount - left_to_pay, + folio=reservation1.folio_id, + partner=reservation1.partner_id, + date=fields.date.today(), + ) + + # ASSERT + self.assertEqual( + reservation1.folio_id.pending_amount, + left_to_pay, + "The pending amount on a partially paid folio it \ + does not correspond to the amount that it should", + ) + + def test_reservation_type_folio(self): + """ + Check that the reservation_type of a folio with + a reservation with the default reservation_type is equal + to 'normal'. + --------------- + A folio is created. A reservation is created to which the + value of the folio_id is the id of the previously created + folio. Then it is verified that the value of the reservation_type + field of the folio is 'normal'. + """ + # ARRANGE AND ACT + self.partner1 = self.env["res.partner"].create({"name": "Ana"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner1.id, + } + ) + + self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "folio_id": folio1.id, + } + ) + + # ASSERT + self.assertEqual( + folio1.reservation_type, + "normal", + "The default reservation type of the folio should be 'normal'", + ) + + def test_invoice_status_staff_reservation(self): + """ + Check that the value of the invoice_status field is 'no' + on a page with reservation_type equal to 'staff'. + ------------ + A reservation is created with the reservation_type field + equal to 'staff'. Then it is verified that the value of + the invoice_status field of the folio created with the + reservation is equal to 'no'. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + self.partner1 = self.env["res.partner"].create({"name": "Pedro"}) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "reservation_type": "staff", + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertEqual( + reservation.folio_id.invoice_status, + "no", + "The invoice status of the folio in a staff reservation should be 'no' ", + ) + + def test_invoice_status_out_reservation(self): + """ + Check that the value of the invoice_status field is 'no' + on a page with reservation_type equal to 'out'. + ------------ + A reservation is created with the reservation_type field + equal to 'out'. Then it is verified that the value of + the invoice_status field of the folio created with the + reservation is equal to 'no'. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + self.partner1 = self.env["res.partner"].create({"name": "Pedro"}) + closure_reason = self.env["room.closure.reason"].create( + { + "name": "test closure reason", + "description": "test clopsure reason description", + } + ) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "reservation_type": "out", + "closure_reason_id": closure_reason.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertEqual( + reservation.folio_id.invoice_status, + "no", + "The invoice status of the folio in a out reservation should be 'no' ", + ) + + def test_amount_total_staff_reservation(self): + """ + Check that the amount_total field of the folio whose + reservation has the reservation_type field as staff + is not calculated. + ------------------------- + A folio is created. A reservation is created to which the + value of the folio_id is the id of the previously created + folio and the field reservation_type equal to 'staff'. Then + it is verified that the value of the amount_total field of + the folio is 0. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + self.partner1 = self.env["res.partner"].create({"name": "Pedro"}) + # ACT + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner1.id, + } + ) + self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "checkin": checkin, + "checkout": checkout, + "folio_id": folio1.id, + "reservation_type": "staff", + } + ) + # ASSERT + self.assertEqual( + folio1.amount_total, + 0.0, + "The amount total of the folio in a staff reservation should be 0", + ) + + def test_amount_total_out_reservation(self): + """ + Check that the amount_total field of the folio whose + reservation has the reservation_type field as out + is not calculated. + ------------------------- + A folio is created. A reservation is created to which the + value of the folio_id is the id of the previously created + folio and the field reservation_type equal to 'out'. Then + it is verified that the value of the amount_total field of + the folio is 0. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + self.partner1 = self.env["res.partner"].create({"name": "Pedro"}) + # ACT + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner1.id, + } + ) + self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "checkin": checkin, + "checkout": checkout, + "folio_id": folio1.id, + "reservation_type": "out", + } + ) + # ASSERT + self.assertEqual( + folio1.amount_total, + 0.0, + "The amount total of the folio in a out of service reservation should be 0", + ) + + def test_reservation_type_incongruence(self): + """ + Check that a reservation cannot be created + with the reservation_type field different from the + reservation_type of its folio. + ------------- + A folio is created. A reservation is created to which the + value of the folio_id is the id of the previously created + folio and the field reservation_type by default('normal'). + Then it is tried to create another reservation with its + reservation_type equal to 'staff'. But it should throw an + error because the value of the reservation_type of the + folio is equal to 'normal'. + """ + self.partner1 = self.env["res.partner"].create({"name": "Ana"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner1.id, + } + ) + + self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "folio_id": folio1.id, + } + ) + with self.assertRaises( + ValidationError, + msg="You cannot create reservations with different reservation_type for a folio", + ): + self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "folio_id": folio1.id, + "reservation_type": "staff", + } + ) + + def _test_create_partner_in_folio(self): + """ + Check that a res_partner is created from a folio. + ------------ + A folio is created by adding the property_id a res.partner + should be created, which is what is checked after creating + the folio. + """ + # ACT + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": "Savannah Byles", + } + ) + # ASSERT + self.assertTrue(folio1.partner_id.id, "The partner has not been created") + + def test_auto_complete_partner_mobile(self): + """ + It is checked that the mobile field of the folio + is correctly added to + a res.partner that exists in + the DB are put in the folio. + -------------------- + A res.partner is created with the name, mobile and email fields. + Then it is checked that the mobile of the res.partner and that of + the folio are the same. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "name": "Enrique", + "mobile": "654667733", + "email": "enrique@example.com", + } + ) + self.id_category = self.env["res.partner.id_category"].create( + {"name": "DNI", "code": "D"} + ) + # ACT + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": partner.id, + } + ) + # ASSERT + self.assertEqual( + folio1.mobile, + partner.mobile, + "The partner mobile has not autocomplete in folio", + ) + + def test_auto_complete_partner_email(self): + """ + It is checked that the email field of the folio + is correctly added to + a res.partner that exists in + the DB are put in the folio. + -------------------- + A res.partner is created with the name, mobile and email fields. + Then it is checked that the email of the res.partner and that of + the folio are the same. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "name": "Simon", + "mobile": "654667733", + "email": "simon@example.com", + } + ) + self.id_category = self.env["res.partner.id_category"].create( + {"name": "DNI", "code": "D"} + ) + + # ACT + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": partner.id, + } + ) + # ASSERT + self.assertEqual( + folio1.email, + partner.email, + "The partner mobile has not autocomplete in folio", + ) + + def test_is_possible_customer_by_email(self): + """ + It is checked that the field is_possible_existing_customer_id + exists in a folio with an email from a res.partner saved + in the DB. + ---------------- + A res.partner is created with the name and email fields. A folio + is created by adding the same email as the res.partner. Then it is + checked that the field is_possible_existing_customer_id is equal to True. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "name": "Courtney Campbell", + "email": "courtney@example.com", + } + ) + # ACT + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner.name, + "email": partner.email, + } + ) + # ASSERT + self.assertTrue( + folio1.possible_existing_customer_ids, "No customer found with this email" + ) + + def test_is_possible_customer_by_mobile(self): + """ + It is checked that the field is_possible_existing_customer_id + exists in a folio with a mobile from a res.partner saved + in the DB. + ---------------- + A res.partner is created with the name and email fields. A folio + is created by adding the same mobile as the res.partner. Then it is + checked that the field is_possible_existing_customer_id is equal to True. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "name": "Ledicia Sandoval", + "mobile": "615369231", + } + ) + # ACT + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner.name, + "mobile": partner.mobile, + } + ) + # ASSERT + self.assertTrue( + folio1.possible_existing_customer_ids, + "No customer found with this mobile", + ) + + def test_add_possible_customer(self): + """ + Check that a partner was correctly added to the folio + after launching the add_partner() method of the several partners wizard + --------------- + A res.partner is created with name, email and mobile. A folio is created. + The wizard is created with the folio id and the partner added to the + possible_existing_customer_ids field. The add_partner method of the wizard + is launched and it is checked that the partner was correctly added to the + folio. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "name": "Serafín Rivas", + "email": "serafin@example.com", + "mobile": "60595595", + } + ) + + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner.name, + "email": partner.email, + } + ) + + several_partners_wizard = self.env["pms.several.partners.wizard"].create( + { + "folio_id": folio1.id, + "possible_existing_customer_ids": [(6, 0, [partner.id])], + } + ) + # ACT + several_partners_wizard.add_partner() + # ASSERT + self.assertEqual( + folio1.partner_id.id, + partner.id, + "The partner was not added to the folio ", + ) + + def test_not_add_several_possibles_customers(self): + """ + Check that multiple partners cannot be added to a folio + from the several partners wizard. + --------------- + Two res.partner are created with name, email and mobile. A folio is created. + The wizard is created with the folio id and the two partners added to the + possible_existing_customer_ids field. The add_partner method of the wizard + is launched and it is verified that a Validation_Error was raised. + """ + # ARRANGE + partner1 = self.env["res.partner"].create( + { + "name": "Serafín Rivas", + "email": "serafin@example.com", + "mobile": "60595595", + } + ) + partner2 = self.env["res.partner"].create( + { + "name": "Simon", + "mobile": "654667733", + "email": "simon@example.com", + } + ) + + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "email": partner1.email, + } + ) + + several_partners_wizard = self.env["pms.several.partners.wizard"].create( + { + "folio_id": folio1.id, + "possible_existing_customer_ids": [(6, 0, [partner1.id, partner2.id])], + } + ) + + # ACT AND ASSERT + with self.assertRaises( + ValidationError, + msg="Two partners cannot be added to the folio", + ): + several_partners_wizard.add_partner() + + def test_not_add_any_possibles_customers(self): + """ + Check that the possible_existing_customer_ids field of the several + partners wizard can be left empty and then launch the add_partner() + method of this wizard to add a partner in folio. + --------------- + A folio is created. The wizard is created without the + possible_existing_customer_ids field. The add_partner method of + the wizard is launched and it is verified that a Validation_Error + was raised. + """ + + # ARRANGE + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": "Rosa Costa", + } + ) + + several_partners_wizard = self.env["pms.several.partners.wizard"].create( + { + "folio_id": folio1.id, + } + ) + + # ACT AND ASSERT + with self.assertRaises( + ValidationError, + msg="A partner can be added to the folio", + ): + several_partners_wizard.add_partner() + + def test_add_partner_invoice_contact(self): + """ + Check that when adding a customer at check-in, reservation or folio, + it is added as a possible billing address + --------------- + Three res.partner are created with name, email and mobile. A folio is created. + We add the partners to the folio, reservation, and checkin, and check that the + three partners are on partner_invoice in folio. + """ + # ARRANGE + partner1 = self.env["res.partner"].create( + { + "name": "Serafín Rivas", + "email": "serafin@example.com", + "mobile": "60595595", + } + ) + partner2 = self.env["res.partner"].create( + { + "name": "Simon", + "mobile": "654667733", + "email": "simon@example.com", + } + ) + partner3 = self.env["res.partner"].create( + { + "name": "Sofia", + "mobile": "688667733", + "email": "sofia@example.com", + } + ) + + # FIRST ACTION + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "email": partner1.email, + } + ) + reservation1 = self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + + # FIRST ASSERT + self.assertEqual( + len(folio1.partner_invoice_ids), + 0, + "A partner was added as a billing contact for no reason", + ) + + # SECOND ACTION + folio1.partner_id = partner1.id + + # SECOND ASSERT + self.assertEqual( + folio1.partner_invoice_ids.ids, + [partner1.id], + "A folio partner was not added as a billing contact", + ) + + # SECOND ACTION + reservation1.partner_id = partner2.id + + # SECOND ASSERT + self.assertIn( + partner2.id, + folio1.partner_invoice_ids.ids, + "A reservation partner was not added as a billing contact", + ) + + # THIRD ACTION + reservation1.checkin_partner_ids[0].partner_id = partner3.id + + # THIRD ASSERT + self.assertIn( + partner3.id, + folio1.partner_invoice_ids.ids, + "A checkin partner was not added as a billing contact", + ) + + @freeze_time("2001-10-10") + def test_folio_sale_channel_origin_in_reservation(self): + """ + Check that the reservation has sale_channel_origin_id + as the folio sale_channel_origin_id in + which reservation was created + + When a reservation is created on a folio + that already has a sale_channel_origin + that reservation will have the same sale_channel_origin + + """ + # ARRANGE + partner1 = self.env["res.partner"].create({"name": "partner1"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ACT + reservation1 = self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + # ASSERT + self.assertEqual( + reservation1.sale_channel_origin_id.id, + folio1.sale_channel_origin_id.id, + "Sale channel of reservation must be the same that it folio", + ) + + @freeze_time("2001-10-19") + def test_folio_sale_channel_ids(self): + """ + Check if sale_channel_ids of folio correspond to + sale_channel_origin_id of its reservations at the + time of creating a new reservation in the folio + """ + # ARRANGE + sale_channel_phone = self.env["pms.sale.channel"].create( + { + "name": "phone", + "channel_type": "direct", + } + ) + partner1 = self.env["res.partner"].create({"name": "partner1"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + "sale_channel_origin_id": sale_channel_phone.id, + } + ) + # ACT + expected_sale_channels = [] + for reservation in folio1.reservation_ids: + expected_sale_channels.append(reservation.sale_channel_origin_id.id) + + # ASSERT + self.assertItemsEqual( + folio1.sale_channel_ids.ids, + list(set(expected_sale_channels)), + "Sale_channel_ids of folio must be the same as " + "sale_channel_origin of its reservation ", + ) + + @freeze_time("2001-10-22") + def test_folio_sale_channel_ids_reservations_several_origin(self): + """ + Check that sale_channel_ids of folio correspond to sale_channel_origin_id + of its reservations + + In this case, folio1 has two reservations(reservation1, reservation2) + with the same sale_channel_origin. + + sale_channel_origin_id sale_channel_ids + ------------------------- + Folio1 --------> sale_channel_direct1 || sale_channel_direct1 + reservation1 --> sale_channel_direct1 + reservation2 --> sale_channel_direct1 + + Then, reservation2 update sale_channel_origin_id for a diferent one. So the folio + has several reservations with different sale_channel_origin_id. + It should be noted that the check would force having to update + the folio sale_channel_origin_id (force_update_origin) isn't marked. + + Expected result: + + sale_channel_origin_id sale_channel_ids + ---------------------- + Folio1 --------> sale_channel_direct1 | (sale_channel_direct1, sale_channel_phone) + reservation1 --> sale_channel_direct1 + reservation2 --> sale_channel_phone + + In this test case, sale_channel_ids will be checked + """ + # ARRANGE + sale_channel_phone = self.env["pms.sale.channel"].create( + { + "name": "phone", + "channel_type": "direct", + } + ) + partner1 = self.env["res.partner"].create({"name": "partner1"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + reservation2 = self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + # ACT + reservation_vals = { + "sale_channel_origin_id": sale_channel_phone.id, + "force_update_origin": False, + } + + reservation2.write(reservation_vals) + expected_sale_channels = [] + for reservation in folio1.reservation_ids: + expected_sale_channels.append(reservation.sale_channel_origin_id.id) + + # ASSERT + self.assertItemsEqual( + folio1.sale_channel_ids.ids, + list(set(expected_sale_channels)), + "Sale_channel_ids of folio must be the same as " + "sale_channel_origin of its reservation ", + ) + + @freeze_time("2001-10-22") + def test_sale_channel_origin_id_reservation_not_update_origin(self): + """ + Check that sale_channel_origin_id of folio doesn't change + when sale_channel_origin_id of one of its reservations is updated + but the check isn't checked + + In this case, folio1 has two reservations(reservation1, reservation2) + with the same sale_channel_origin. + + sale_channel_origin_id + ------------------------- + Folio1 --------> sale_channel_direct1 + reservation1 --> sale_channel_direct1 + reservation2 --> sale_channel_direct1 + + Then, reservation2 update sale_channel_origin_id for a diferent one. So the folio + has several reservations with different sale_channel_origin_id. + And the check would force having to update + the folio sale_channel_origin_id (force_update_origin) isn't marked. + So sale_channel_origin_id of folio shouldn't change. + + Expected result: + + sale_channel_origin_id + ------------------------- + Folio1 --------> sale_channel_direct1 + reservation1 --> sale_channel_direct1 + reservation2 --> sale_channel_phone + + In this test case, sale_channel_origin_id of folio will be checked + """ + # ARRANGE + sale_channel_phone = self.env["pms.sale.channel"].create( + { + "name": "phone", + "channel_type": "direct", + } + ) + partner1 = self.env["res.partner"].create({"name": "partner1"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + reservation2 = self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + # ACT + reservation_vals = { + "sale_channel_origin_id": sale_channel_phone.id, + "force_update_origin": False, + } + reservation2.write(reservation_vals) + + # ASSERT + self.assertNotEqual( + folio1.sale_channel_origin_id, + reservation2.sale_channel_origin_id, + "Sale_channel_origin_id of folio shouldn't be the same as " + "sale_channel_origin of reservation2", + ) + + @freeze_time("2001-10-25") + def test_sale_channel_origin_id_reservation_update_origin(self): + """ + Check that sale_channel_origin_id of the folio changes when + you change sale_channel_origin_id of one of its reservations + and check that forces the update of sale_channel_origin_id of folio + + + sale_channel_origin_id + ------------------------- + Folio1 --------> sale_channel_direct1 + reservation1 --> sale_channel_direct1 + reservation2 --> sale_channel_direct1 + + Then, reservation2 update sale_channel_origin_id for a diferent one. So the folio + has several reservations with different sale_channel_origin_id. + And the check would force having to update + the folio sale_channel_origin_id (force_update_origin) is marked. + So sale_channel_origin_id of folio must change. + + Expected result: + + sale_channel_origin_id + ------------------------- + Folio1 --------> sale_channel_phone + reservation1 --> sale_channel_phone + reservation2 --> sale_channel_phone + + In this test case, sale_channel_origin_id of folio1 will be checked + """ + # ARRANGE + sale_channel_phone = self.env["pms.sale.channel"].create( + { + "name": "phone", + "channel_type": "direct", + } + ) + partner1 = self.env["res.partner"].create({"name": "partner1"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + reservation2 = self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + # ACT + reservation_vals = { + "sale_channel_origin_id": sale_channel_phone.id, + "force_update_origin": True, + } + reservation2.write(reservation_vals) + # ASSERT + self.assertEqual( + folio1.sale_channel_origin_id, + reservation2.sale_channel_origin_id, + "Sale_channel_origin_id of folio should be updated", + ) + + @freeze_time("2001-10-25") + def test_sale_channel_origin_id_reservation_update_reservations(self): + """ + Check that sale_channel_origin_id of a reservation changes when + another reservation of the same folio changes sale_channel_origin_id + and marks the check. + By changing sale_channel_origin_ id of a reservation and marking the check + that forces the update, changes both sale_channel_origin of folio and + sale_channel_origin of reservations that had the same + + + sale_channel_origin_id + ------------------------- + Folio1 --------> sale_channel_direct1 + reservation1 --> sale_channel_direct1 + reservation2 --> sale_channel_direct1 + + Then, reservation2 update sale_channel_origin_id for a diferent one. + And the check would force having to update + the folio sale_channel_origin_id (force_update_origin) is marked. + So sale_channel_origin_id of folio and other reservations with the same + sale_channel_origin must change. + + Expected result: + + sale_channel_origin_id + ------------------------- + Folio1 --------> sale_channel_phone + reservation1 --> sale_channel_phone + reservation2 --> sale_channel_phone + + In this test case, sale_channel_origin_id of reservation1 will be checked + """ + # ARRANGE + sale_channel_phone = self.env["pms.sale.channel"].create( + { + "name": "phone", + "channel_type": "direct", + } + ) + partner1 = self.env["res.partner"].create({"name": "partner1"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + reservation1 = self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + reservation2 = self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "folio_id": folio1.id, + } + ) + # ACT + reservation_vals = { + "sale_channel_origin_id": sale_channel_phone.id, + "force_update_origin": True, + } + reservation2.write(reservation_vals) + + # ASSERT + self.assertEqual( + reservation1.sale_channel_origin_id, + reservation2.sale_channel_origin_id, + "sale_channel_origin_id of reservations that coincided " + "with sale_channel_origin_id of folio de should be updated", + ) + + def test_pms_folio_form_creation(self): + folio_form = Form(self.env["pms.folio"]) + self.assertFalse(folio_form.possible_existing_customer_ids) diff --git a/pms/tests/test_pms_folio_invoice.py b/pms/tests/test_pms_folio_invoice.py new file mode 100644 index 0000000000..ef8387f26e --- /dev/null +++ b/pms/tests/test_pms_folio_invoice.py @@ -0,0 +1,929 @@ +import datetime + +from odoo import fields + +from .common import TestPms + + +class TestPmsFolioInvoice(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + # create a room type availability + cls.room_type_availability = cls.env["pms.availability.plan"].create( + {"name": "Availability plan for TEST"} + ) + + # journal to simplified invoices + cls.simplified_journal = cls.env["account.journal"].create( + { + "name": "Simplified journal", + "code": "SMP", + "type": "sale", + "company_id": cls.env.ref("base.main_company").id, + } + ) + + # create a property + cls.property = cls.env["pms.property"].create( + { + "name": "MY PMS TEST", + "company_id": cls.env.ref("base.main_company").id, + "default_pricelist_id": cls.pricelist1.id, + "journal_simplified_invoice_id": cls.simplified_journal.id, + } + ) + + # create room type + cls.room_type_double = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.property.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + "price": 25, + } + ) + + # create rooms + cls.room1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.property.id, + "name": "Double 101", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + } + ) + + cls.room2 = cls.env["pms.room"].create( + { + "pms_property_id": cls.property.id, + "name": "Double 102", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + } + ) + + cls.room3 = cls.env["pms.room"].create( + { + "pms_property_id": cls.property.id, + "name": "Double 103", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + } + ) + + # res.partner + cls.partner_id = cls.env["res.partner"].create( + { + "name": "Miguel", + "vat": "45224522J", + "country_id": cls.env.ref("base.es").id, + "city": "Madrid", + "zip": "28013", + "street": "Calle de la calle", + } + ) + + # create a sale channel + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + + def create_configuration_accounting_scenario(self): + """ + Method to simplified scenario to payments and accounting: + # REVIEW: + - Use new property with odoo demo data company to avoid account configuration + - Emule SetUp with new property: + - create demo_room_type_double + - Create 2 rooms room_type_double + """ + self.pms_property_demo = self.env["pms.property"].create( + { + "name": "Property Based on Comapany Demo", + "company_id": self.env.ref("base.main_company").id, + "default_pricelist_id": self.env.ref("product.list0").id, + } + ) + # create room type + self.demo_room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property_demo.id], + "name": "Double Test", + "default_code": "Demo_DBL_Test", + "class_id": self.room_type_class1.id, + "price": 25, + } + ) + # create rooms + self.double1 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property_demo.id, + "name": "Double 101", + "room_type_id": self.demo_room_type_double.id, + "capacity": 2, + } + ) + self.double2 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property_demo.id, + "name": "Double 102", + "room_type_id": self.demo_room_type_double.id, + "capacity": 2, + } + ) + # make current journals payable + journals = self.env["account.journal"].search( + [ + ("type", "in", ["bank", "cash"]), + ] + ) + journals.allowed_pms_payments = True + + def _test_invoice_full_folio(self): + """ + Check that when launching the create_invoices() method for a full folio, + the invoice_status field is set to "invoiced". + ---------------- + A reservation is created. The create_invoices() method of the folio of + that reservation is launched. It is verified that the invoice_status field + of the folio is equal to "invoiced". + """ + # ARRANGE + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + state_expected = "invoiced" + # ACT + r1.folio_id._create_invoices() + r1.flush() + # ASSERT + self.assertEqual( + state_expected, + r1.folio_id.invoice_status, + "The status after a full invoice folio isn't correct", + ) + + def _test_invoice_partial_folio_by_steps(self): + """ + Check that when launching the create_invoices() method for a partial folio, + the invoice_status field is set to "invoiced". + ---------------- + A reservation is created. The create_invoices() method of the folio of + that reservation is launched with the first sale line. It is verified + that the invoice_status field of the folio is equal to "invoiced". + """ + # ARRANGE + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + dict_lines = dict() + + dict_lines[ + r1.folio_id.sale_line_ids.filtered(lambda l: not l.display_type)[0].id + ] = 3 + r1.folio_id._create_invoices(lines_to_invoice=dict_lines) + + self.assertEqual( + "invoiced", + r1.folio_id.invoice_status, + "The status after an invoicing is not correct", + ) + + def test_invoice_partial_folio_diferent_partners(self): + # ARRANGE + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + dict_lines = dict() + # qty to 1 to 1st folio sale line + dict_lines[ + r1.folio_id.sale_line_ids.filtered(lambda l: not l.display_type)[0].id + ] = 1 + r1.folio_id._create_invoices( + lines_to_invoice=dict_lines, + partner_invoice_id=self.env.ref("base.res_partner_1").id, + ) + + # test does not work without invalidating cache + self.env["account.move"].invalidate_cache() + + self.assertNotEqual( + "invoiced", + r1.folio_id.invoice_status, + "The status after a partial invoicing is not correct", + ) + # qty to 2 to 1st folio sale line + dict_lines[ + r1.folio_id.sale_line_ids.filtered(lambda l: not l.display_type)[0].id + ] = 2 + r1.folio_id._create_invoices( + lines_to_invoice=dict_lines, + partner_invoice_id=self.env.ref("base.res_partner_12").id, + ) + self.assertNotEqual( + r1.folio_id.move_ids.mapped("partner_id")[0], + r1.folio_id.move_ids.mapped("partner_id")[1], + "The status after an invoicing is not correct", + ) + + def test_amount_invoice_folio(self): + """ + Test create and invoice from the Folio, and check amount of the reservation. + ------------- + A reservation is created. The create_invoices() method is launched and it is + verified that the total amount of the reservation folio is equal to the total + of the created invoice. + """ + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + total_amount_expected = r1.folio_id.amount_total + r1.folio_id._create_invoices() + self.assertEqual( + r1.folio_id.move_ids.amount_total, + total_amount_expected, + "Total amount of the invoice and total amount of folio don't match", + ) + + def test_qty_to_invoice_folio(self): + """ + Test create and invoice from the Folio, and check qty to invoice. + ---------------------- + A reservation is created.Then it is verified that the total quantity + to be invoice from the sale lines of the reservation folio corresponds + to expected. + """ + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + qty_to_invoice_expected = sum( + r1.folio_id.sale_line_ids.mapped("qty_to_invoice") + ) + self.assertEqual( + qty_to_invoice_expected, + 3.0, + "The quantity to be invoice on the folio does not correspond", + ) + + def test_qty_invoiced_folio(self): + """ + Test create and invoice from the Folio, and check qty invoiced. + --------------- + A reservation is created.The create_invoices() method is launched and it is + verified that the total quantity invoiced of the reservation folio is equal + to the total quantity of the created invoice. + """ + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.folio_id._create_invoices() + qty_invoiced_expected = sum(r1.folio_id.sale_line_ids.mapped("qty_invoiced")) + self.assertEqual( + qty_invoiced_expected, + 3.0, + "The quantity invoiced on the folio does not correspond", + ) + + def test_price_invoice_by_services_folio(self): + """ + Test create and invoice from the Folio, and check amount in a + specific segment of services. + """ + + self.product1 = self.env["product.product"].create( + {"name": "Test Product 1", "per_day": True, "list_price": 10} + ) + + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": self.product1.id, + "reservation_id": self.reservation1.id, + } + ) + + dict_lines = dict() + dict_lines[ + self.reservation1.folio_id.sale_line_ids.filtered("service_id")[0].id + ] = 3 + self.reservation1.folio_id._create_invoices(lines_to_invoice=dict_lines) + self.assertEqual( + self.reservation1.folio_id.sale_line_ids.filtered("service_id")[ + 0 + ].price_total, + self.reservation1.folio_id.move_ids.amount_total, + "The service price don't match between folio and invoice", + ) + + def test_qty_invoiced_by_services_folio(self): + """ + Test create and invoice from the Folio, and check qty invoiced + in a specific segment of services + """ + + self.product1 = self.env["product.product"].create( + {"name": "Test Product 1", "per_day": True, "list_price": 10} + ) + + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": self.product1.id, + "reservation_id": self.reservation1.id, + } + ) + + dict_lines = dict() + service_lines = self.reservation1.folio_id.sale_line_ids.filtered("service_id") + for line in service_lines: + dict_lines[line.id] = 1 + self.reservation1.folio_id._create_invoices(lines_to_invoice=dict_lines) + expected_qty_invoiced = sum( + self.reservation1.folio_id.move_ids.invoice_line_ids.mapped("quantity") + ) + self.assertEqual( + expected_qty_invoiced, + sum(self.reservation1.folio_id.sale_line_ids.mapped("qty_invoiced")), + "The quantity of invoiced services don't match between folio and invoice", + ) + + def test_qty_to_invoice_by_services_folio(self): + """ + Test create an invoice from the Folio, and check qty to invoice + in a specific segment of services + """ + + self.product1 = self.env["product.product"].create( + {"name": "Test Product 1", "per_day": True, "list_price": 10} + ) + + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": self.product1.id, + "reservation_id": self.reservation1.id, + } + ) + + expected_qty_to_invoice = sum( + self.reservation1.folio_id.sale_line_ids.filtered("service_id").mapped( + "qty_to_invoice" + ) + ) + self.assertEqual( + expected_qty_to_invoice, + 3.0, + "The quantity of services to be invoice is wrong", + ) + + def test_price_invoice_board_service(self): + """ + Test create and invoice from the Folio, and check the related + amounts with board service linked + """ + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Test Board Service 1", + "default_code": "CB1", + } + ) + self.board_service_line1 = self.env["pms.board.service.line"].create( + { + "product_id": self.product1.id, + "pms_board_service_id": self.board_service1.id, + "amount": 10, + "adults": True, + } + ) + + self.board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type_double.id, + "pms_board_service_id": self.board_service1.id, + "pms_property_id": self.property.id, + } + ) + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + dict_lines = dict() + dict_lines[ + self.reservation1.folio_id.sale_line_ids.filtered("service_id")[0].id + ] = 1 + self.reservation1.folio_id._create_invoices(lines_to_invoice=dict_lines) + self.assertEqual( + self.reservation1.folio_id.sale_line_ids.filtered("service_id")[ + 0 + ].price_total, + self.reservation1.folio_id.move_ids.amount_total, + "The board service price don't match between folio and invoice", + ) + + def test_qty_invoiced_board_service(self): + """ + Test create and invoice from the Folio, and check qty invoiced + with board service linked. + """ + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Test Board Service 1", + "default_code": "CB1", + } + ) + self.board_service_line1 = self.env["pms.board.service.line"].create( + { + "product_id": self.product1.id, + "pms_board_service_id": self.board_service1.id, + "amount": 10, + "adults": True, + } + ) + + self.board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type_double.id, + "pms_board_service_id": self.board_service1.id, + "pms_property_id": self.property.id, + } + ) + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + dict_lines = dict() + service_lines = self.reservation1.folio_id.sale_line_ids.filtered("service_id") + for line in service_lines: + dict_lines[line.id] = 1 + self.reservation1.folio_id._create_invoices(lines_to_invoice=dict_lines) + expected_qty_invoiced = sum( + self.reservation1.folio_id.move_ids.invoice_line_ids.mapped("quantity") + ) + self.assertEqual( + expected_qty_invoiced, + sum(self.reservation1.folio_id.sale_line_ids.mapped("qty_invoiced")), + "The quantity of invoiced board services don't match between folio and invoice", + ) + + def test_qty_to_invoice_board_service(self): + """ + Test create and invoice from the Folio, and check qty to invoice + with board service linked + """ + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Test Board Service 1", + "default_code": "CB1", + } + ) + self.board_service_line1 = self.env["pms.board.service.line"].create( + { + "product_id": self.product1.id, + "pms_board_service_id": self.board_service1.id, + "amount": 10, + "adults": True, + } + ) + + self.board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type_double.id, + "pms_board_service_id": self.board_service1.id, + "pms_property_id": self.property.id, + } + ) + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + dict_lines = dict() + service_lines = self.reservation1.folio_id.sale_line_ids.filtered("service_id") + for line in service_lines: + dict_lines[line.id] = 1 + self.reservation1.folio_id._create_invoices(lines_to_invoice=dict_lines) + expected_qty_to_invoice = sum( + self.reservation1.folio_id.sale_line_ids.filtered("service_id").mapped( + "qty_to_invoice" + ) + ) + self.assertEqual( + expected_qty_to_invoice, + 0, + "The quantity of board services to be invoice is wrong", + ) + + def test_autoinvoice_folio_checkout_property_policy(self): + """ + Test create and invoice the cron by property preconfig automation + -------------------------------------- + Set property default_invoicing_policy to checkout with 0 days with + margin, and check that the folio autoinvoice date is set to last checkout + folio date + """ + # ARRANGE + self.property.default_invoicing_policy = "checkout" + self.property.margin_days_autoinvoice = 0 + + # ACT + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT + self.assertIn( + datetime.date.today() + datetime.timedelta(days=3), + self.reservation1.folio_id.mapped("sale_line_ids.autoinvoice_date"), + "The autoinvoice date in folio with property checkout policy is wrong", + ) + + def test_autoinvoice_folio_checkout_partner_policy(self): + """ + Test create and invoice the cron by partner preconfig automation + -------------------------------------- + Set partner invoicing_policy to checkout with 2 days with + margin, and check that the folio autoinvoice date is set to last checkout + folio date + 2 days + """ + # ARRANGE + self.partner_id.invoicing_policy = "checkout" + self.partner_id.margin_days_autoinvoice = 2 + + # ACT + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.date.today(), + "checkout": datetime.date.today() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + self.reservation1.reservation_line_ids.default_invoice_to = self.partner_id + + # ASSERT + self.assertEqual( + datetime.date.today() + datetime.timedelta(days=5), + self.reservation1.folio_id.sale_line_ids.filtered( + lambda l: l.invoice_status == "to_invoice" + )[0].autoinvoice_date, + "The autoinvoice date in folio with property checkout policy is wrong", + ) + + def test_autoinvoice_paid_folio_overnights_partner_policy(self): + """ + Test create and invoice the cron by partner preconfig automation + with partner setted as default invoiced to in reservation lines + -------------------------------------- + Set partner invoicing_policy to checkout, create a reservation + with room, board service and normal service, run autoinvoicing + method and check that only room and board service was invoiced + in partner1, the folio must be paid + + """ + # ARRANGE + self.create_configuration_accounting_scenario() + self.partner_id2 = self.env["res.partner"].create( + { + "name": "Sara", + "vat": "54235544A", + "country_id": self.env.ref("base.es").id, + "city": "Madrid", + "zip": "28013", + "street": "Street 321", + } + ) + self.partner_id.invoicing_policy = "checkout" + self.partner_id.margin_days_autoinvoice = 0 + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + self.product2 = self.env["product.product"].create( + { + "name": "Test Product 2", + "lst_price": 100, + } + ) + + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Test Board Service 1", + "default_code": "CB1", + "amount": 10, + } + ) + + self.board_service_line1 = self.env["pms.board.service.line"].create( + { + "product_id": self.product1.id, + "pms_board_service_id": self.board_service1.id, + "amount": 10, + "adults": True, + } + ) + + self.board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.demo_room_type_double.id, + "pms_board_service_id": self.board_service1.id, + "pms_property_id": self.pms_property_demo.id, + } + ) + # ACT + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property_demo.id, + "checkin": datetime.date.today() - datetime.timedelta(days=3), + "checkout": datetime.date.today(), + "adults": 2, + "room_type_id": self.demo_room_type_double.id, + "partner_id": self.partner_id2.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + self.service = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": self.product2.id, + "reservation_id": self.reservation1.id, + } + ) + folio = self.reservation1.folio_id + reservation1 = self.reservation1 + reservation1.reservation_line_ids.default_invoice_to = self.partner_id + reservation1.service_ids.filtered( + "is_board_service" + ).default_invoice_to = self.partner_id + + folio.do_payment( + journal=self.env["account.journal"].browse( + reservation1.folio_id.pms_property_id._get_payment_methods().ids[0] + ), + receivable_account=self.env["account.journal"] + .browse(reservation1.folio_id.pms_property_id._get_payment_methods().ids[0]) + .suspense_account_id, + user=self.env.user, + amount=reservation1.folio_id.pending_amount, + folio=folio, + partner=reservation1.partner_id, + date=fields.date.today(), + ) + self.pms_property_demo.autoinvoicing() + + # ASSERT + overnight_sale_lines = self.reservation1.folio_id.sale_line_ids.filtered( + lambda line: line.reservation_line_ids or line.is_board_service + ) + partner_invoice = self.reservation1.folio_id.move_ids.filtered( + lambda inv: inv.partner_id == self.partner_id + ) + self.assertEqual( + partner_invoice.mapped("line_ids.folio_line_ids.id"), + overnight_sale_lines.ids, + "Billed services and overnights invoicing wrong compute", + ) + + def test_not_autoinvoice_unpaid_cancel_folio_partner_policy(self): + """ + Test create and invoice the cron by partner preconfig automation + -------------------------------------- + Set partner invoicing_policy to checkout, create a reservation + with room, board service and normal service, run autoinvoicing + method and check that not invoice was created becouse + the folio is cancel and not paid + """ + # ARRANGE + self.partner_id.invoicing_policy = "checkout" + self.partner_id.margin_days_autoinvoice = 0 + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + self.product2 = self.env["product.product"].create( + { + "name": "Test Product 2", + "lst_price": 100, + } + ) + + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Test Board Service 1", + "default_code": "CB1", + "amount": 10, + } + ) + + self.board_service_line1 = self.env["pms.board.service.line"].create( + { + "product_id": self.product1.id, + "pms_board_service_id": self.board_service1.id, + "amount": 10, + "adults": True, + } + ) + + self.board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type_double.id, + "pms_board_service_id": self.board_service1.id, + "pms_property_id": self.property.id, + } + ) + # ACT + self.reservation1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.date.today() - datetime.timedelta(days=3), + "checkout": datetime.date.today(), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_id.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + self.service = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": self.product2.id, + "reservation_id": self.reservation1.id, + } + ) + self.reservation1.action_cancel() + self.property.autoinvoicing() + + # ASSERT + partner_invoice = self.reservation1.folio_id.move_ids.filtered( + lambda inv: inv.partner_id == self.partner_id + ) + self.assertEqual( + partner_invoice.mapped("line_ids.folio_line_ids.id"), + [], + "Billed services and overnights invoicing wrong compute", + ) + + def _test_invoice_line_group_by_room_type_sections(self): + """Test create and invoice from the Folio, and check qty invoice/to invoice, + and the grouped invoice lines by room type, by one + line by unit prices/qty with nights""" + + def _test_downpayment(self): + """Test invoice qith a way of downpaument and check dowpayment's + folio line is created and also check a total amount of invoice is + equal to a respective folio's total amount""" + + def _test_invoice_with_discount(self): + """Test create with a discount and check discount applied + on both Folio lines and an inovoice lines""" + + def _test_reinvoice(self): + """Test the compute reinvoice folio take into account + nights and services qty invoiced""" diff --git a/pms/tests/test_pms_folio_prices.py b/pms/tests/test_pms_folio_prices.py new file mode 100644 index 0000000000..da7af4e005 --- /dev/null +++ b/pms/tests/test_pms_folio_prices.py @@ -0,0 +1,11 @@ +from odoo.tests.common import SavepointCase + + +class TestPmsFolioPrice(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_price_folio(self): + """Test create reservation and services, and check price + tax and discounts""" diff --git a/pms/tests/test_pms_folio_sale_line.py b/pms/tests/test_pms_folio_sale_line.py new file mode 100644 index 0000000000..42261b23b1 --- /dev/null +++ b/pms/tests/test_pms_folio_sale_line.py @@ -0,0 +1,1300 @@ +import datetime + +from odoo import fields + +from .common import TestPms + + +class TestPmsFolioSaleLine(TestPms): + @classmethod + def setUpClass(cls): + """ + - common + room_type_avalability_plan + """ + super().setUpClass() + + # create room type + cls.room_type_double = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + "price": 25, + } + ) + # create room + cls.room1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 101", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + } + ) + cls.room2 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 102", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + } + ) + + cls.product_test1 = cls.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + } + ) + cls.product_test2 = cls.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + } + ) + cls.board_service_test = cls.board_service = cls.env[ + "pms.board.service" + ].create( + { + "name": "Test Board Service", + "default_code": "TPS", + } + ) + cls.env["pms.board.service.line"].create( + { + "pms_board_service_id": cls.board_service_test.id, + "product_id": cls.product_test1.id, + "amount": 8, + "adults": True, + } + ) + cls.board_service_room_type = cls.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": cls.room_type_double.id, + "pms_board_service_id": cls.board_service_test.id, + "pms_property_id": cls.pms_property1.id, + } + ) + cls.extra_service = cls.env["pms.service"].create( + { + "is_board_service": False, + "product_id": cls.product_test2.id, + } + ) + + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + + # RESERVATION LINES + def test_comp_fsl_rooms_all_same_group(self): + """ + check the grouping of the reservation lines on the sale line folio + when the price, discount match- + ------------ + reservation with 3 nights with the same price, + should generate just 1 reservation sale line + """ + # ARRANGE + expected_sale_lines = 1 + + # ACT + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "reservation_line_ids": [ + ( + 0, + False, + { + "date": fields.date.today(), + "price": 20, + "discount": 10, + }, + ), + ( + 0, + False, + { + "date": fields.date.today() + datetime.timedelta(days=1), + "price": 20, + "discount": 10, + }, + ), + ( + 0, + False, + { + "date": fields.date.today() + datetime.timedelta(days=2), + "price": 20, + "discount": 10, + }, + ), + ], + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT + self.assertEqual( + expected_sale_lines, + len(r_test.folio_id.sale_line_ids.filtered(lambda x: not x.display_type)), + "Folio should contain {} sale lines".format(expected_sale_lines), + ) + + def test_comp_fsl_rooms_different_prices(self): + """ + Check that a reservation with two nights and different prices per + night generates two sale lines. + ------------ + Create a reservation with a double room as a room type and 2 nights, + which has a price of 25.0 per night. Then the price of one of the reservation + lines is changed to 50.0. As there are two different prices per + night in the reservation the sale lines of the folio should be 2 . + """ + + # ARRANGE + expected_sale_lines = 2 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + r_test.reservation_line_ids[0].price = 50.0 + + # ASSERT + self.assertEqual( + expected_sale_lines, + len(r_test.folio_id.sale_line_ids.filtered(lambda x: not x.display_type)), + "Folio should contain {} reservation sale lines".format( + expected_sale_lines + ), + ) + + def test_comp_fsl_rooms_different_discount(self): + """ + Check that a reservation with two nights and different discount per + night generates two sale lines. + ------------ + Create a reservation with a double room as a room type and 2 nights, which has + a default discount of 0 per night. Then the discount of one of the reservation + lines is changed to 50.0. As there are two different discounts per night in the + reservation the sale lines of the folio should be 2. + """ + + # ARRANGE + expected_sale_lines = 2 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + r_test.reservation_line_ids[0].discount = 50.0 + + # ASSERT + self.assertEqual( + expected_sale_lines, + len(r_test.folio_id.sale_line_ids.filtered(lambda x: not x.display_type)), + "Folio should contain {} reservation sale lines".format( + expected_sale_lines + ), + ) + + def test_comp_fsl_rooms_different_cancel_discount(self): + """ + Check that a reservation with two nights and different cancel + discount per night generates two sale lines. + ------------ + Create a reservation with a double room as a room type and 2 nights, + which has a default cancel discount of 0 per night. Then the cancel discount + of one of the reservation lines is changed to 50.0. As there are two + different cancel discount per night in the reservation the sale lines of + the folio should be 2. As one of the reservation lines has a 100% cancel + discount, the sale line should be 1 . + """ + + # ARRANGE + expected_sale_lines = 2 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + r_test.reservation_line_ids[0].cancel_discount = 50.0 + + # ASSERT + self.assertEqual( + expected_sale_lines, + len(r_test.folio_id.sale_line_ids.filtered(lambda x: not x.display_type)), + "Folio should contain {} reservation sale lines".format( + expected_sale_lines + ), + ) + + def test_comp_fsl_rooms_one_full_cancel_discount(self): + """ + Check that a reservation with a 100% cancel discount on one night + does not generate different sale lines. + ---------------- + Create a reservation with a double room as a room type, which has + a default cancel discount of 0 per night. Then the cancel discount + of one of the reservation lines is changed to 100.0. + """ + # ARRANGE + expected_sale_lines = 1 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + r_test.reservation_line_ids[0].cancel_discount = 100.0 + r_test.flush() + + # ASSERT + self.assertEqual( + expected_sale_lines, + len(r_test.folio_id.sale_line_ids.filtered(lambda x: not x.display_type)), + "Folio should contain {} reservation sale lines".format( + expected_sale_lines + ), + ) + + def test_comp_fsl_rooms_increase_stay(self): + """ + Check when adding a night to a reservation after creating it and this night + has the same price, cancel and cancel discount values, the sales line that + were created with the reservation are maintained. + --------- + Create a reservation of 2 nights for a double room. The value of the sale lines + of that reservation is stored in a variable. Then one more night is added to the + reservation and it is verified that the sale lines are the same as the value of + the previously saved variable. + """ + # ARRANGE + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.flush() + previous_folio_sale_line = r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type + )[0] + + # ACT + r_test.checkout = datetime.datetime.now() + datetime.timedelta(days=4) + r_test.flush() + + # ASSERT + self.assertEqual( + previous_folio_sale_line, + r_test.folio_id.sale_line_ids.filtered(lambda x: not x.display_type)[0], + "Previous records of reservation sales lines should not be " + "deleted if it is not necessary", + ) + + def test_comp_fsl_rooms_decrease_stay(self): + """ + Check when a night is removed from a reservation after creating + it, the sales lines that were created with the reservation are kept. + --------- + Create a reservation of 2 nights for a double room. The value of the sale lines + of that reservation is stored in a variable. Then it is removed one night at + reservation and it is verified that the reservation sale lines are equal to the value of + the previously saved variable. + """ + # ARRANGE + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.flush() + previous_folio_sale_line = r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type + )[0] + + # ACT + r_test.checkout = datetime.datetime.now() + datetime.timedelta(days=2) + r_test.flush() + + # ASSERT + self.assertEqual( + previous_folio_sale_line, + r_test.folio_id.sale_line_ids.filtered(lambda x: not x.display_type)[0], + "Previous records of reservation sales lines should not be " + "deleted if it is not necessary", + ) + + def test_comp_fsl_rooms_same_stay(self): + """ + Check that when changing the price of all the reservation lines in a + reservation, which before the change had the same price, discount + and cancel discount values, the same sale lines that existed before + the change are kept. + ------------------ + Create a reservation of 2 nights for a double room with a price of 25.0. + The value of the sale lines of that reservation is stored in a variable. + Then the value of the price of all the reservation lines is changed to 50.0 + and it is verified that the reservation sale lines are equal to the value + of the previously saved variable. + """ + # ARRANGE + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.flush() + previous_folio_sale_line = r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type + )[0] + + # ACT + r_test.reservation_line_ids.price = 50 + r_test.flush() + + # ASSERT + self.assertEqual( + previous_folio_sale_line, + r_test.folio_id.sale_line_ids.filtered(lambda x: not x.display_type)[0], + "Previous records of reservation sales lines should not be " + "deleted if it is not necessary", + ) + + # BOARD SERVICES + def test_comp_fsl_board_services_all_same_group(self): + + """ + Check that the board services of reservation with the same price, discount + and cancel discount values, should only generate one sale line. + ---------------- + Create a reservation of 2 nights, for a double room with a board service + room per night. Then it is verified that the length of the sale lines of the + board services in the reservation is equal to 1. + """ + # ARRANGE + expected_board_service_sale_lines = 1 + + # ACT + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "board_service_room_id": self.board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT + self.assertEqual( + expected_board_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.reservation_id and x.service_id and x.is_board_service + ) + ), + "Folio should contain {} board service sale lines".format( + expected_board_service_sale_lines + ), + ) + + def test_comp_fsl_board_services_different_prices(self): + """ + Check that the board services of reservation with different prices + should generate several sale lines. + ---------------- + Create a reservation of 2 nights, for a double room with a board service + room per night. Then change the price of the first board service line to + 1.0 and it is verified that the length of the sale lines of the board services + in the reservation is equal to 2 because there are 2 different board service + prices in the reservation. + """ + # ARRANGE + expected_board_service_sale_lines = 2 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "board_service_room_id": self.board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.service_ids[0].service_line_ids[0].price_unit = 1.0 + + # ASSERT + self.assertEqual( + expected_board_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + ) + ), + "Folio should contain {} board service sale lines".format( + expected_board_service_sale_lines + ), + ) + + def test_comp_fsl_board_services_different_discount(self): + """ + Check that the board services of reservation with different discounts + should generate several sale lines. + ---------------- + Create a reservation of 2 nights, for a double room with a board service + room per night. Then change the discount of the first board service line + to 1.0 and it is verified that the length of the sale lines of the board services + in the reservation is equal to 2 because there are 2 different board service + discounts in the reservation. + """ + # ARRANGE + expected_board_service_sale_lines = 2 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "board_service_room_id": self.board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + r_test.service_ids[0].service_line_ids[0].discount = 1.0 + + # ASSERT + self.assertEqual( + expected_board_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + ) + ), + "Folio should contain {} board service sale lines".format( + expected_board_service_sale_lines + ), + ) + + def test_comp_fsl_board_services_different_cancel_discount(self): + """ + Check that the board services of reservation with different cancel + discounts should generate several sale lines. + ---------------- + Create a reservation of 2 nights, for a double room with a board service + room per night. Then change the cancel discount of the first board service line + to 1.0 and it is verified that the length of the sale lines of the board services + in the reservation is equal to 2 because there are 2 different board service + cancel discounts in the reservation. + """ + + # ARRANGE + expected_board_service_sale_lines = 2 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "board_service_room_id": self.board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + r_test.service_ids[0].service_line_ids[0].cancel_discount = 1.0 + + # ASSERT + self.assertEqual( + expected_board_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + ) + ), + "Folio should contain {} board service sale lines".format( + expected_board_service_sale_lines + ), + ) + + def test_comp_fsl_board_services_one_full_cancel_discount(self): + """ + Check that the board services of reservation with 100% cancel + discount should generate only 1 sale line. + ---------------- + Create a reservation of 2 nights, for a double room with a board service + room per night. Then change the cancel discount of the first board service line + to 100.0 and it is verified that the length of the sale lines of the board services + in the reservation is equal to 1. + """ + + # ARRANGE + expected_board_service_sale_lines = 1 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "board_service_room_id": self.board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + r_test.service_ids[0].service_line_ids[0].cancel_discount = 100.0 + + # ASSERT + self.assertEqual( + expected_board_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + ) + ), + "Folio should contain {} board service sale lines".format( + expected_board_service_sale_lines + ), + ) + + def test_comp_fsl_board_services_increase_stay(self): + """ + Check when adding a night to a reservation with board services room, + after creating it and this board service has the same price, cancel + and cancel discount values, the sale lines that were created with the + reservation are kept. + --------- + Create a reservation of 2 nights for a double room with a board service. + The value of the sale lines of that board services is stored in a variable. + Then one more night is added to the reservation and it is verified that + the sale lines are the same as the value of the previously saved variable. + """ + + # ARRANGE + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "board_service_room_id": self.board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + previous_folio_board_service_sale_line = r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + )[0] + + # ACT + r_test.checkout = datetime.datetime.now() + datetime.timedelta(days=4) + + # ASSERT + self.assertEqual( + previous_folio_board_service_sale_line, + r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + )[0], + "Previous records of board service sales lines should not be " + "deleted if it is not necessary", + ) + + def test_comp_fsl_board_services_decrease_stay(self): + """ + Check when removing a night to a reservation with board services room, + after creating it and this board service has the same price, cancel + and cancel discount values, the sale lines that were created with the + reservation are kept. + --------- + Create a reservation of 2 nights for a double room with a board service. + The value of the sale lines of that board services is stored in a variable. + Then one night is removed to the reservation and it is verified that + the sale lines are the same as the value of the previously saved variable. + """ + + # ARRANGE + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "board_service_room_id": self.board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + previous_folio_board_service_sale_line = r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + )[0] + + # ACT + r_test.checkout = datetime.datetime.now() + datetime.timedelta(days=2) + + # ASSERT + self.assertEqual( + previous_folio_board_service_sale_line, + r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + )[0], + "Previous records of board service sales lines should not be " + "deleted if it is not necessary", + ) + + def test_comp_fsl_board_services_same_stay(self): + """ + Check that when changing the price of all board services in a + reservation, which before the change had the same price, discount + and cancel discount values, the same sale lines that existed before + the change are kept. + ------------------ + Create a reservation of 2 nights for a double room with a board service + price of 8.0. The value of the sale lines of the board services is stored + in a variable. Then the value of the price of all the reservation board services + is changed to 50 and it is verified that the reservation sale lines are equal to + the value of the previously saved variable. + """ + # ARRANGE + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "board_service_room_id": self.board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + previous_folio_board_service_sale_line = r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + )[0] + + # ACT + r_test.service_ids.filtered( + lambda x: x.is_board_service + ).service_line_ids.price_unit = 50 + + # ASSERT + self.assertEqual( + previous_folio_board_service_sale_line, + r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.display_type and x.is_board_service + )[0], + "Previous records of board service sales lines should not be " + "deleted if it is not necessary", + ) + + # RESERVATION EXTRA DAILY SERVICES + def test_comp_fsl_res_extra_services_all_same_group(self): + """ + Check that when adding a service that is not a board service to a + reservation with the same price, cancel and cancel discount, the + number of sales lines is kept. + ------------------ + Create a 2 night reservation. Then a service is added with + is_board_service = False and it is verified that the length of + the sale lines of the reservation is 1. + """ + # ARRANGE + expected_extra_service_sale_lines = 1 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ACT + r_test.service_ids = [(4, self.extra_service.id)] + r_test.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + expected_extra_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + ) + ), + "Folio should contain {} reservation service sale lines".format( + expected_extra_service_sale_lines + ), + ) + + def test_comp_fsl_res_extra_services_different_prices(self): + """ + Check that a reservation of several nights and with different + prices per day on services should generate several sale lines. + ----------------- + Create a reservation for 2 nights. Then add a service to this + reservation and the price of the first service line is changed + to 44.5. It is verified that the length of the reservation's sale + lines is equal to 2, because there are two different prices per day + for service lines. + """ + + # ARRANGE + expected_extra_service_sale_lines = 2 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.service_ids = [(4, self.extra_service.id)] + r_test.service_ids.service_line_ids.flush() + + # ACT + r_test.service_ids.service_line_ids[0].price_unit = 44.5 + r_test.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + expected_extra_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + ) + ), + "Folio should contain {} reservation service sale lines".format( + expected_extra_service_sale_lines + ), + ) + + def test_comp_fsl_res_extra_services_different_discount(self): + """ + Check that a reservation of several nights and with different + discount per day on services should generate several sale lines. + ----------------- + Create a reservation for 2 nights. Then add a service to this + reservation and the discount of the first service line is changed + to 44.5. It is verified that the length of the reservation's sale + lines is equal to 2, because there are two different discounts per day + for service lines. + """ + + # ARRANGE + expected_extra_service_sale_lines = 2 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.service_ids = [(4, self.extra_service.id)] + r_test.service_ids.service_line_ids.flush() + + # ACT + r_test.service_ids.service_line_ids[0].discount = 44.5 + r_test.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + expected_extra_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + ) + ), + "Folio should contain {} reservation service sale lines".format( + expected_extra_service_sale_lines + ), + ) + + def test_comp_fsl_res_extra_services_different_cancel_discount(self): + """ + Check that a reservation of several nights and with different + cancel discount per day on services should generate several sale + lines. + ----------------- + Create a reservation for 2 nights. Then add a service to this + reservation and the cancel discount of the first service line is changed + to 44.5. It is verified that the length of the reservation's sale + lines is equal to 2, because there are two different cancel discounts per + day for service lines. + """ + + # ARRANGE + expected_extra_service_sale_lines = 2 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.service_ids = [(4, self.extra_service.id)] + r_test.service_ids.service_line_ids.flush() + + # ACT + r_test.service_ids.service_line_ids[0].cancel_discount = 44.5 + r_test.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + expected_extra_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + ) + ), + "Folio should contain {} reservation service sale lines".format( + expected_extra_service_sale_lines + ), + ) + + def test_comp_fsl_res_extra_services_one_full_cancel_discount(self): + """ + Check that a reservation of several nights and with a 100% cancel + discount for a service should generate only 1 sale line. + ----------------- + Create a reservation for 2 nights. Then add a service to this + reservation and the cancel discount of the first service line is changed + to 100%. It is verified that the length of the reservation sale + lines is equal to 1. + """ + + # ARRANGE + expected_extra_service_sale_lines = 1 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.service_ids = [(4, self.extra_service.id)] + r_test.service_ids.service_line_ids.flush() + + # ACT + r_test.service_ids.service_line_ids[0].cancel_discount = 100 + r_test.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + expected_extra_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + ) + ), + "Folio should contain {} reservation service sale lines".format( + expected_extra_service_sale_lines + ), + ) + + def test_comp_fsl_res_extra_services_increase_stay(self): + """ + Check when adding a night to a reservation after creating it and this services + has the same price, cancel and cancel discount values, the sales line that + were created with the reservation are maintained. + --------- + Create a reservation of 2 nights for a double room and add a service to this + reservation. The value of the sale lines of that reservation services is stored + in a variable. Then one more night is added to the reservation and it is verified + that the reservation service sale lines are the same as the value of the previously + saved variable. + """ + + # ARRANGE + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.service_ids = [(4, self.extra_service.id)] + r_test.service_ids.service_line_ids.flush() + previous_folio_extra_service_sale_line = r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + )[0] + + # ACT + r_test.checkout = datetime.datetime.now() + datetime.timedelta(days=4) + r_test.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + previous_folio_extra_service_sale_line, + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + ), + "Previous records of reservation service sales lines should not be " + "deleted if it is not necessary", + ) + + def test_comp_fsl_res_extra_services_decrease_stay(self): + """ + Check when removing a night to a reservation after creating it and this services + has the same price, cancel and cancel discount values, the sales line that + were created with the reservation are maintained. + --------- + Create a reservation of 2 nights for a double room and add a service to this + reservation. The value of the sale lines of the services is stored + in a variable. Then one night is removed to the reservation and it is verified + that the reservation service sale lines are the same as the value of the previously + saved variable. + """ + # ARRANGE + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.service_ids = [(4, self.extra_service.id)] + r_test.service_ids.service_line_ids.flush() + previous_folio_extra_service_sale_line = r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + )[0] + + # ACT + r_test.checkout = datetime.datetime.now() + datetime.timedelta(days=2) + r_test.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + previous_folio_extra_service_sale_line, + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + ), + "Previous records of reservation service sales lines should not be " + "deleted if it is not necessary", + ) + + def test_comp_fsl_res_extra_services_same_stay(self): + # TEST CASE + # Price is changed for all reservation services of a 2-night reservation. + # But price, discount & cancel discount after the change is the same + # for all nights. + # Should keep the same reservation service sales line record. + """ + Check that when changing the price of all services in a + reservation, which before the change had the same price, discount + and cancel discount values, the same sale lines that existed before + the change are kept. + ------------------ + Create a reservation of 2 nights for a double room and add a service to this + reservation. The value of the sale lines of the services is stored + in a variable. Then the value of the price of all the reservation services + is changed to 50 and it is verified that the reservation service sale lines + are equal to the value of the previously saved variable. + """ + + # ARRANGE + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.service_ids = [(4, self.extra_service.id)] + r_test.service_ids.service_line_ids.flush() + previous_folio_extra_service_sale_line = r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + )[0] + + # ACT + r_test.service_ids.filtered( + lambda x: x.id == self.extra_service.id + ).service_line_ids.price_unit = 50 + r_test.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + previous_folio_extra_service_sale_line, + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + ), + "Previous records of reservation service sales lines should not be " + "deleted if it is not necessary", + ) + + # FOLIO EXTRA SERVICES + def test_comp_fsl_fol_extra_services_one(self): + # TEST CASE + # Folio with extra services + # should generate 1 folio service sale line + """ + Check that when adding a service that is not a board service to a + folio with the same price, cancel and cancel discount, the number + of sales lines is kept. + ------------------ + Create a 2 night reservation. Then a service is added with + is_board_service = False and it is verified that the length of + the sale lines of the folio is 1. + """ + # ARRANGE + expected_folio_service_sale_lines = 1 + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + r_test.folio_id.service_ids = [(4, self.extra_service.id)] + r_test.folio_id.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + expected_folio_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: x.service_id == self.extra_service + ) + ), + "Folio should contain {} folio service sale lines".format( + expected_folio_service_sale_lines + ), + ) + + def test_comp_fsl_fol_extra_services_two(self): + """ + Check that when adding several services to a folio, + several sale lines should be generated on the folio. + ----------------- + Create a 2 night reservation. Two services are added + to the reservation and it is verified that the length + of the folio sale lines is equal to 2. + """ + + # ARRANGE + expected_folio_service_sale_lines = 2 + product_test2 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + extra_service2 = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": product_test2.id, + } + ) + + # ACT + r_test.folio_id.service_ids = [(4, self.extra_service.id)] + r_test.folio_id.service_ids = [(4, extra_service2.id)] + r_test.folio_id.service_ids.service_line_ids.flush() + + # ASSERT + self.assertEqual( + expected_folio_service_sale_lines, + len( + r_test.folio_id.sale_line_ids.filtered( + lambda x: not x.reservation_id and not x.display_type + ) + ), + "Folio should contain {} folio service sale lines".format( + expected_folio_service_sale_lines + ), + ) + + def test_no_sale_lines_staff_reservation(self): + """ + Check that the sale_line_ids of a folio whose reservation + is of type 'staff' are created with price 0. + ----- + A reservation is created with the reservation_type field + with value 'staff'. Then it is verified that the + sale_line_ids of the folio created with the creation of + the reservation have price 0. + """ + # ARRANGE + self.partner1 = self.env["res.partner"].create({"name": "Alberto"}) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "reservation_type": "staff", + "sale_channel_origin_id": self.sale_channel_direct1.id, + "adults": 1, + } + ) + # ASSERT + self.assertEqual( + reservation.folio_id.sale_line_ids.mapped("price_unit")[0], + 0, + "Staff folio sale lines should have price 0", + ) + + def test_no_sale_lines_out_reservation(self): + """ + Check that the sale_line_ids of a folio whose reservation + is of type 'out' are not created. + ----- + A reservation is created with the reservation_type field + with value 'out'. Then it is verified that the + sale_line_ids of the folio created with the creation of + the reservation are equal to False. + """ + # ARRANGE + self.partner1 = self.env["res.partner"].create({"name": "Alberto"}) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + closure_reason = self.env["room.closure.reason"].create( + { + "name": "test closure reason", + "description": "test clopsure reason description", + } + ) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "reservation_type": "out", + "closure_reason_id": closure_reason.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertFalse( + reservation.folio_id.sale_line_ids, + "Folio sale lines should not be generated for a out of service type reservation ", + ) diff --git a/pms/tests/test_pms_invoice_refund.py b/pms/tests/test_pms_invoice_refund.py new file mode 100644 index 0000000000..d532b92194 --- /dev/null +++ b/pms/tests/test_pms_invoice_refund.py @@ -0,0 +1,11 @@ +from freezegun import freeze_time + +from odoo.tests.common import SavepointCase + +freeze_time("2000-02-02") + + +class TestPmsInvoiceRefund(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() diff --git a/pms/tests/test_pms_multiproperty.py b/pms/tests/test_pms_multiproperty.py new file mode 100644 index 0000000000..de81767fd4 --- /dev/null +++ b/pms/tests/test_pms_multiproperty.py @@ -0,0 +1,1065 @@ +import datetime + +from odoo import fields +from odoo.exceptions import UserError + +from .common import TestPms + + +class TestPmsMultiproperty(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.pms_property2 = cls.env["pms.property"].create( + { + "name": "Pms_property_test2", + "company_id": cls.company1.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + + cls.pms_property3 = cls.env["pms.property"].create( + { + "name": "Pms_property_test3", + "company_id": cls.company1.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + + def test_availability_closed_no_room_type_check_property(self): + """ + Check that availability rules are applied to the correct properties. + ---------- + Check that for that date test_property1 doesnt have rooms available + (of that type:room_type1), + instead, property2 has room_type1 available + """ + # ARRANGE + self.pricelist2 = self.env["product.pricelist"].create( + { + "name": "test pricelist 1", + "pms_property_ids": [ + (4, self.pms_property1.id), + (4, self.pms_property2.id), + ], + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + self.availability_plan1 = self.env["pms.availability.plan"].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [self.pricelist2.id])], + "pms_property_ids": [ + (4, self.pms_property1.id), + (4, self.pms_property2.id), + ], + } + ) + self.room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [ + (4, self.pms_property1.id), + (4, self.pms_property2.id), + ], + "name": "Special Room Test", + "default_code": "SP_Test", + "class_id": self.room_type_class1.id, + } + ) + self.room1 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "Double 201 test", + "room_type_id": self.room_type1.id, + "capacity": 2, + } + ) + # pms.room + self.room2 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property2.id, + "name": "Double 202 test", + "room_type_id": self.room_type1.id, + "capacity": 2, + } + ) + self.room_type_availability_rule1 = self.env[ + "pms.availability.plan.rule" + ].create( + { + "availability_plan_id": self.availability_plan1.id, + "room_type_id": self.room_type1.id, + "date": (fields.datetime.today() + datetime.timedelta(days=2)).date(), + "closed": True, + "pms_property_id": self.pms_property1.id, + } + ) + self.room_type_availability_rule2 = self.env[ + "pms.availability.plan.rule" + ].create( + { + "availability_plan_id": self.availability_plan1.id, + "room_type_id": self.room_type1.id, + "date": (fields.datetime.today() + datetime.timedelta(days=2)).date(), + "pms_property_id": self.pms_property2.id, + } + ) + + properties = [ + {"property": self.pms_property1.id, "value": False}, + {"property": self.pms_property2.id, "value": True}, + ] + + for p in properties: + with self.subTest(k=p): + # ACT + pms_property = self.env["pms.property"].browse(p["property"]) + pms_property = pms_property.with_context( + checkin=fields.date.today(), + checkout=( + fields.datetime.today() + datetime.timedelta(days=2) + ).date(), + room_type_id=self.room_type1.id, + pricelist_id=self.pricelist2.id, + ) + rooms_avail = pms_property.free_room_ids + + # ASSERT + self.assertEqual( + len(rooms_avail) > 0, p["value"], "Availability is not correct" + ) + + # AMENITY + def test_amenity_property_not_allowed(self): + """ + Creation of a Amenity with Properties incompatible with it Amenity Type + + +-----------------------------------+-----------------------------------+ + | Amenity Type (TestAmenityType1) | Amenity (TestAmenity1) | + +-----------------------------------+-----------------------------------+ + | Property1 - Property2 | Property1 - Property2 - Property3 | + +-----------------------------------+-----------------------------------+ + """ + # ARRANGE + AmenityType = self.env["pms.amenity.type"] + Amenity = self.env["pms.amenity"] + amenity_type1 = AmenityType.create( + { + "name": "TestAmenityType1", + "pms_property_ids": [ + (4, self.pms_property1.id), + (4, self.pms_property2.id), + ], + } + ) + # ACT & ASSERT + with self.assertRaises(UserError), self.cr.savepoint(): + Amenity.create( + { + "name": "TestAmenity1", + "pms_amenity_type_id": amenity_type1.id, + "pms_property_ids": [ + ( + 6, + 0, + [ + self.pms_property1.id, + self.pms_property2.id, + self.pms_property3.id, + ], + ) + ], + } + ) + + # AVAILABILITY PLAN RULES + def test_check_property_availability_room_type(self): + """ + Check integrity between availability properties and room_type properties. + Test cases when creating a availability_rule: + Allowed properties: + Room Type(room_type1) --> pms_property1, pms_property_4 + Availability Plan(availability_example) --> pms_property1, pms_property2 + + Both cases throw an exception: + # 1:Rule for property2, + # it is allowed in availability_plan but not in room_type + # 2:Rule for property4, + # it is allowed in room_type, but not in availability_plan + """ + # ARRANGE + self.pms_property4 = self.env["pms.property"].create( + { + "name": "Property 3", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + self.pricelist2 = self.env["product.pricelist"].create( + { + "name": "test pricelist 1", + "pms_property_ids": [ + (4, self.pms_property1.id), + (4, self.pms_property2.id), + ], + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + # create new room_type + self.room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [ + (4, self.pms_property1.id), + (4, self.pms_property4.id), + ], + "name": "Special Room Test", + "default_code": "SP_Test", + "class_id": self.room_type_class1.id, + } + ) + # ACT + self.availability_plan1 = self.env["pms.availability.plan"].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [self.pricelist2.id])], + "pms_property_ids": [ + (4, self.pms_property1.id), + (4, self.pms_property2.id), + ], + } + ) + self.availability_rule1 = self.env["pms.availability.plan.rule"].create( + { + "availability_plan_id": self.availability_plan1.id, + "room_type_id": self.room_type1.id, + "date": (fields.datetime.today() + datetime.timedelta(days=2)).date(), + "closed": True, + "pms_property_id": self.pms_property1.id, + } + ) + + test_cases = [ + { + "pms_property_id": self.pms_property2.id, + }, + { + "pms_property_id": self.pms_property4.id, + }, + ] + # ASSERT + for test_case in test_cases: + with self.subTest(k=test_case): + with self.assertRaises(UserError): + self.availability_rule1.pms_property_id = test_case[ + "pms_property_id" + ] + + # BOARD SERVICE LINE + def test_pms_bsl_product_property_integrity(self): + """ + Creation of a board service line without property, of a product + only available for a specific property. + """ + # ARRANGE + product1 = self.env["product.product"].create( + {"name": "Product", "pms_property_ids": [self.pms_property1.id]} + ) + board_service1 = self.env["pms.board.service"].create( + { + "name": "Board Service", + "default_code": "CB", + } + ) + # ACT & ASSERT + with self.assertRaises( + UserError, msg="Board service line shouldnt be created." + ): + self.env["pms.board.service.line"].create( + { + "product_id": product1.id, + "pms_board_service_id": board_service1.id, + "adults": True, + } + ) + + def test_pms_bsl_board_service_property_integrity(self): + """ + Creation of a board service line without property, of board service + only available for a specific property. + """ + # ARRANGE + pms_property2 = self.env["pms.property"].create( + { + "name": "Property 1", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + product1 = self.env["product.product"].create( + {"name": "Product", "pms_property_ids": [self.pms_property1.id]} + ) + + board_service1 = self.env["pms.board.service"].create( + { + "name": "Board Service", + "default_code": "CB", + "pms_property_ids": [pms_property2.id], + } + ) + # ACT & ASSERT + with self.assertRaises( + UserError, msg="Board service line shouldnt be created." + ): + self.env["pms.board.service.line"].create( + { + "product_id": product1.id, + "pms_board_service_id": board_service1.id, + "adults": True, + } + ) + + def test_pms_bsl_board_service_line_prop_integrity(self): + """ + Creation of a board service line with a specific property, + of board service without property. + """ + # ARRANGE + pms_property2 = self.env["pms.property"].create( + { + "name": "Property 1", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + product1 = self.env["product.product"].create( + {"name": "Product", "pms_property_ids": [self.pms_property1.id]} + ) + board_service1 = self.env["pms.board.service"].create( + { + "name": "Board Service", + "default_code": "CB", + } + ) + # ACT & ASSERT + with self.assertRaises( + UserError, msg="Board service line shouldnt be created." + ): + self.env["pms.board.service.line"].create( + { + "product_id": product1.id, + "pms_board_service_id": board_service1.id, + "pms_property_ids": [pms_property2.id], + "adults": True, + } + ) + + # BOARD SERVICE ROOM TYPE + def test_create_rt_props_gt_bs_props(self): + """ + Create board service for a room type and the room type + have MORE properties than the board service. + Record of board_service_room_type should contain the + board service properties. + """ + # ARRANGE + pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id, pms_property2.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + "price": 25, + } + ) + board_service_test = self.board_service = self.env["pms.board.service"].create( + { + "name": "Test Board Service", + "default_code": "TPS", + "pms_property_ids": [self.pms_property1.id], + } + ) + # ACT + new_bsrt = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type_double.id, + "pms_board_service_id": board_service_test.id, + "pms_property_id": self.pms_property1.id, + } + ) + # ASSERT + self.assertIn( + new_bsrt.pms_property_id.id, + board_service_test.pms_property_ids.ids, + "Record of board_service_room_type should contain the" + " board service properties.", + ) + + def test_create_rt_props_lt_bs_props(self): + """ + Create board service for a room type and the room type + have LESS properties than the board service. + Record of board_service_room_type should contain the + room types properties. + """ + # ARRANGE + pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + "price": 25, + } + ) + board_service1 = self.board_service = self.env["pms.board.service"].create( + { + "name": "Test Board Service", + "default_code": "TPS", + "pms_property_ids": [self.pms_property1.id, pms_property2.id], + } + ) + # ACT + new_bsrt = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type1.id, + "pms_board_service_id": board_service1.id, + "pms_property_id": self.pms_property1.id, + } + ) + # ASSERT + self.assertIn( + new_bsrt.pms_property_id.id, + room_type1.pms_property_ids.ids, + "Record of board_service_room_type should contain the" + " room types properties.", + ) + + def _test_create_rt_props_eq_bs_props(self): + """ + Create board service for a room type and the room type + have THE SAME properties than the board service. + Record of board_service_room_type should contain the + room types properties that matchs with the board + service properties + """ + # ARRANGE + room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + "price": 25, + } + ) + board_service1 = self.board_service = self.env["pms.board.service"].create( + { + "name": "Test Board Service", + "default_code": "TPS", + "pms_property_ids": [self.pms_property1.id], + } + ) + # ACT + new_bsrt = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type1.id, + "pms_board_service_id": board_service1.id, + "pms_property_id": self.pms_property1.id, + } + ) + # ASSERT + self.assertIn( + new_bsrt.pms_property_ids.ids == room_type1.pms_property_ids.ids + and new_bsrt.pms_property_ids.ids == board_service1.pms_property_ids.ids, + "Record of board_service_room_type should contain the room " + "types properties and matchs with the board service properties", + ) + + def test_create_rt_no_props_and_bs_props(self): + """ + Create board service for a room type and the room type + hasn't properties but the board services. + Record of board_service_room_type should contain the + board service properties. + """ + # ARRANGE + room_type1 = self.env["pms.room.type"].create( + { + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + "price": 25, + } + ) + board_service1 = self.board_service = self.env["pms.board.service"].create( + { + "name": "Test Board Service", + "default_code": "TPS", + "pms_property_ids": [self.pms_property1.id], + } + ) + # ACT + new_bsrt = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type1.id, + "pms_board_service_id": board_service1.id, + "pms_property_id": self.pms_property1.id, + } + ) + # ASSERT + self.assertIn( + new_bsrt.pms_property_id.id, + board_service1.pms_property_ids.ids, + "Record of board_service_room_type should contain the" + " board service properties.", + ) + + def test_create_rt_props_and_bs_no_props(self): + """ + Create board service for a room type and the board service + hasn't properties but the room type. + Record of board_service_room_type should contain the + room type properties. + """ + # ARRANGE + pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id, pms_property2.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + "price": 25, + } + ) + board_service1 = self.board_service = self.env["pms.board.service"].create( + { + "name": "Test Board Service", + "default_code": "TPS", + "pms_property_ids": [self.pms_property1.id], + } + ) + # ACT + new_bsrt = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type1.id, + "pms_board_service_id": board_service1.id, + "pms_property_id": self.pms_property1.id, + } + ) + # ASSERT + self.assertIn( + new_bsrt.pms_property_id.id, + room_type1.pms_property_ids.ids, + "Record of board_service_room_type should contain the" + " room type properties.", + ) + + def test_create_rt_no_props_and_bs_no_props(self): + """ + Create board service for a room type and the board service + has no properties and neither does the room type + Record of board_service_room_type shouldnt contain properties. + """ + # ARRANGE + + room_type1 = self.env["pms.room.type"].create( + { + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + "price": 25, + } + ) + board_service1 = self.board_service = self.env["pms.board.service"].create( + { + "name": "Test Board Service", + "default_code": "TPS", + } + ) + # ACT + new_bsrt = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type1.id, + "pms_board_service_id": board_service1.id, + } + ) + # ASSERT + self.assertFalse( + new_bsrt.pms_property_id.id, + "Record of board_service_room_type shouldnt contain properties.", + ) + + def test_pms_bsrtl_product_property_integrity(self): + """ + Creation of a board service room type line without property, of a product + only available for a specific property. + """ + # ARRANGE + + product1 = self.env["product.product"].create( + {"name": "Product", "pms_property_ids": self.pms_property1} + ) + board_service1 = self.env["pms.board.service"].create( + { + "name": "Board Service", + "default_code": "CB", + } + ) + room_type1 = self.env["pms.room.type"].create( + { + "name": "Room Type", + "default_code": "Type1", + "class_id": self.room_type_class1.id, + } + ) + board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_board_service_id": board_service1.id, + "pms_room_type_id": room_type1.id, + } + ) + + # ACT & ASSERT + with self.assertRaises( + UserError, msg="Board service room type line shouldnt be created." + ): + self.env["pms.board.service.room.type.line"].create( + { + "pms_board_service_room_type_id": board_service_room_type1.id, + "product_id": product1.id, + } + ) + + def test_pms_bsrtl_board_service_line_prop_integrity(self): + """ + Creation of a board service room type line with a specific property, + of board service without property. + """ + # ARRANGE + product1 = self.env["product.product"].create( + {"name": "Product", "pms_property_ids": [self.pms_property1.id]} + ) + board_service1 = self.env["pms.board.service"].create( + { + "name": "Board Service", + "default_code": "CB", + } + ) + + room_type1 = self.env["pms.room.type"].create( + { + "name": "Room Type", + "default_code": "Type1", + "class_id": self.room_type_class1.id, + } + ) + board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_board_service_id": board_service1.id, + "pms_room_type_id": room_type1.id, + } + ) + + # ACT & ASSERT + with self.assertRaises( + UserError, msg="Board service line shouldnt be created." + ): + self.env["pms.board.service.room.type.line"].create( + { + "product_id": product1.id, + "pms_property_id": self.pms_property2.id, + "pms_board_service_room_type_id": board_service_room_type1.id, + } + ) + + # PMS.FOLIO + def test_folio_closure_reason_consistency_properties(self): + """ + Check the multiproperty consistency between + clousure reasons and folios + ------- + create multiproperty scenario (3 properties in total) and + a new clousure reason in pms_property1 and pms_property2, then, create + a new folio in property3 and try to set the clousure_reason + waiting a error property consistency. + """ + # ARRANGE + cl_reason = self.env["room.closure.reason"].create( + { + "name": "closure_reason_test", + "pms_property_ids": [ + (4, self.pms_property1.id), + (4, self.pms_property2.id), + ], + } + ) + + # ACTION & ASSERT + with self.assertRaises( + UserError, + msg="Folio created with clousure_reason_id with properties inconsistence", + ): + self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property3.id, + "closure_reason_id": cl_reason.id, + } + ) + + # PRICELIST + def test_inconsistency_property_pricelist_item(self): + """ + Check a pricelist item and its pricelist are inconsistent with the property. + Create a pricelist item that belongs to a property and check if + a pricelist that belongs to a diferent one, cannot be created. + """ + # ARRANGE + # ACT & ASSERT + self.pricelist2 = self.env["product.pricelist"].create( + { + "name": "test pricelist 1", + "pms_property_ids": [ + (4, self.pms_property1.id), + (4, self.pms_property2.id), + ], + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + self.room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "name": "Single", + "default_code": "SIN", + "class_id": self.room_type_class1.id, + "list_price": 30, + } + ) + with self.assertRaises(UserError): + self.item1 = self.env["product.pricelist.item"].create( + { + "name": "item_1", + "applied_on": "0_product_variant", + "product_id": self.room_type1.product_id.id, + "date_start": datetime.datetime.today(), + "date_end": datetime.datetime.today() + datetime.timedelta(days=1), + "fixed_price": 40.0, + "pricelist_id": self.pricelist2.id, + "pms_property_ids": [self.pms_property3.id], + } + ) + + def test_inconsistency_cancelation_rule_property(self): + """ + Check a cancelation rule and its pricelist are inconsistent with the property. + Create a cancelation rule that belongs to a two properties and check if + a pricelist that belongs to a diferent properties, cannot be created. + """ + # ARRANGE + + Pricelist = self.env["product.pricelist"] + # ACT + self.cancelation_rule1 = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "pms_property_ids": [self.pms_property1.id, self.pms_property3.id], + } + ) + # ASSERT + with self.assertRaises(UserError): + Pricelist.create( + { + "name": "Pricelist Test", + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "cancelation_rule_id": self.cancelation_rule1.id, + "is_pms_available": True, + } + ) + + def test_inconsistency_availability_plan_property(self): + """ + Check a availability plan and its pricelist are inconsistent with the property. + Create a availability plan that belongs to a two properties and check if + a pricelist that belongs to a diferent properties, cannot be created. + """ + self.availability_plan1 = self.env["pms.availability.plan"].create( + {"name": "Availability Plan", "pms_property_ids": [self.pms_property1.id]} + ) + with self.assertRaises(UserError): + self.env["product.pricelist"].create( + { + "name": "Pricelist", + "pms_property_ids": [self.pms_property2.id], + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + + def _test_multiproperty_checks(self): + """ + # TEST CASE + Multiproperty checks in reservation + +---------------+------+------+------+----+----+ + | reservation | property1 | + +---------------+------+------+------+----+----+ + | room | property2 | + | room_type | property2, property3 | + | board_service | property2, property3 | + | pricelist | property2, property3 | + +---------------+------+------+------+----+----+ + """ + # ARRANGE + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Board Service Test", + "default_code": "CB", + } + ) + host1 = self.env["res.partner"].create( + { + "name": "Miguel", + "mobile": "654667733", + "email": "miguel@example.com", + } + ) + self.sale_channel_direct1 = self.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + self.reservation1 = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "pms_property_id": self.pms_property1.id, + "partner_id": host1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + room_type_test = self.env["pms.room.type"].create( + { + "pms_property_ids": [ + (4, self.pms_property3.id), + (4, self.pms_property2.id), + ], + "name": "Single", + "default_code": "SIN", + "class_id": self.room_type_class1.id, + "list_price": 30, + } + ) + + room = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property2.id, + "room_type_id": room_type_test.id, + } + ) + + pricelist2 = self.env["product.pricelist"].create( + { + "name": "pricelist_test", + "pms_property_ids": [ + (4, self.pms_property2.id), + (4, self.pms_property3.id), + ], + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + + board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_board_service_id": self.board_service1.id, + "pms_room_type_id": room_type_test.id, + "pms_property_ids": [self.pms_property2.id, self.pms_property3.id], + } + ) + test_cases = [ + {"preferred_room_id": room.id}, + {"room_type_id": room_type_test.id}, + {"pricelist_id": pricelist2.id}, + {"board_service_room_id": board_service_room_type1.id}, + ] + + for test_case in test_cases: + with self.subTest(k=test_case): + with self.assertRaises(UserError): + self.reservation1.write(test_case) + + # ROOM + def test_inconsistency_room_ubication_property(self): + """ + Room property and its ubication properties are inconsistent. + A Room with property that is not included in available properties + for its ubication cannot be created. + """ + # ARRANGE + self.room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "name": "Single", + "default_code": "SI", + "class_id": self.room_type_class1.id, + "list_price": 30, + } + ) + ubication1 = self.env["pms.ubication"].create( + { + "name": "UbicationTest", + "pms_property_ids": [ + (4, self.pms_property1.id), + ], + } + ) + # ACT & ASSERT + with self.assertRaises( + UserError, + msg="The room should not be created if its property is not included " + "in the available properties for its ubication.", + ): + self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property2.id, + "room_type_id": self.room_type1.id, + "ubication_id": ubication1.id, + } + ) + + def test_consistency_room_ubication_property(self): + """ + Room property and its ubication properties are consistent. + A Room with property included in available properties + for its ubication can be created. + """ + # ARRANGE + self.room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "name": "Single", + "default_code": "SI", + "class_id": self.room_type_class1.id, + "list_price": 30, + } + ) + ubication1 = self.env["pms.ubication"].create( + { + "name": "UbicationTest", + "pms_property_ids": [ + (4, self.pms_property1.id), + ], + } + ) + # ACT + new_room1 = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + "ubication_id": ubication1.id, + } + ) + # ASSERT + self.assertIn( + new_room1.pms_property_id, + ubication1.pms_property_ids, + "The room should be created if its property belongs to the availabe" + "properties for its ubication.", + ) + + def test_inconsistency_room_type_property(self): + """ + Room property and its room type properties are inconsistent. + A Room with property that is not included in available properties + for its room type cannot be created. + """ + # ARRANGE + self.pms_property3 = self.env["pms.property"].create( + { + "name": "Property_3", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + self.room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "name": "Single", + "default_code": "SI", + "class_id": self.room_type_class1.id, + "list_price": 30, + } + ) + # ACT & ARRANGE + with self.assertRaises( + UserError, + msg="The room should not be created if its property is not included " + "in the available properties for its room type.", + ): + self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property3.id, + "room_type_id": self.room_type1.id, + } + ) + + def test_consistency_room_type_property(self): + """ + Room property and its room type properties are inconsistent. + A Room with property included in available properties + for its room type can be created. + """ + # ARRANGE + self.room_type1 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "name": "Single", + "default_code": "SI", + "class_id": self.room_type_class1.id, + "list_price": 30, + } + ) + # ACT + room1 = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + } + ) + # ASSERT + self.assertIn( + room1.pms_property_id, + self.room_type1.pms_property_ids, + "The room should be created if its property is included " + "in the available properties for its room type.", + ) diff --git a/pms/tests/test_pms_payment.py b/pms/tests/test_pms_payment.py new file mode 100644 index 0000000000..79f42d4cb1 --- /dev/null +++ b/pms/tests/test_pms_payment.py @@ -0,0 +1,39 @@ +from freezegun import freeze_time + +from odoo.tests.common import SavepointCase + +freeze_time("2000-02-02") + + +class TestPmsPayment(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # TODO: Test allowed manual payment + # create a journal with allowed_pms_payments = True and + # check that the _get_payment_methods property method return it + + # TODO: Test not allowed manual payment + # create a journal without allowed_pms_payments = True and + # check that the _get_payment_methods property method dont return it + + # TODO: Test default account payment create + # create a bank journal, a reservation, pay the reservation + # with do_payment method without pay_type parameter + # and check that account payment was created + + # TODO: Test default statement line create + # create a cash journal, a reservation, pay the reservation + # with do_payment method without pay_type parameter + # and check that statement line was created + + # TODO: Test set pay_type cash, statement line create + # create a bank journal, a reservation, pay the reservation + # with do_payment method with 'cash' pay_type parameter + # and check that statement line was created + + # TODO: Test set pay_type bank, account payment create + # create a cash journal, a reservation, pay the reservation + # with do_payment method with 'bank' pay_type parameter + # and check that account payment was created diff --git a/pms/tests/test_pms_pricelist.py b/pms/tests/test_pms_pricelist.py new file mode 100644 index 0000000000..76bb8cfac4 --- /dev/null +++ b/pms/tests/test_pms_pricelist.py @@ -0,0 +1,1272 @@ +import datetime + +from freezegun import freeze_time + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests import tagged + +from .common import TestPms + + +@tagged("standard", "nice") +class TestPmsPricelist(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.pms_property2 = cls.env["pms.property"].create( + { + "name": "Property_2", + "company_id": cls.env.ref("base.main_company").id, + "default_pricelist_id": cls.env.ref("product.list0").id, + } + ) + + cls.pms_property3 = cls.env["pms.property"].create( + { + "name": "Property_3", + "company_id": cls.env.ref("base.main_company").id, + "default_pricelist_id": cls.env.ref("product.list0").id, + } + ) + + cls.room_type1 = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id, cls.pms_property2.id], + "name": "Single", + "default_code": "SIN", + "class_id": cls.room_type_class1.id, + "list_price": 30, + } + ) + + # pms.room + cls.room1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Single 101", + "room_type_id": cls.room_type1.id, + "capacity": 2, + } + ) + + cls.pricelist2 = cls.env["product.pricelist"].create( + { + "name": "pricelist_2", + "pms_property_ids": [cls.pms_property1.id, cls.pms_property2.id], + "availability_plan_id": cls.availability_plan1.id, + "is_pms_available": True, + } + ) + # product.product 1 + cls.product1 = cls.env["product.product"].create({"name": "Test Breakfast"}) + + # pms.board.service + cls.board_service1 = cls.env["pms.board.service"].create( + { + "name": "Test Only Breakfast", + "default_code": "CB1", + } + ) + # pms.board.service.line + cls.board_service_line1 = cls.env["pms.board.service.line"].create( + { + "product_id": cls.product1.id, + "pms_board_service_id": cls.board_service1.id, + "adults": True, + } + ) + + # pms.board.service.room.type + cls.board_service_room_type1 = cls.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": cls.room_type1.id, + "pms_board_service_id": cls.board_service1.id, + "pms_property_id": cls.pms_property1.id, + } + ) + + cls.partner1 = cls.env["res.partner"].create({"name": "Carles"}) + + # create a sale channel + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + + @freeze_time("2000-01-01") + def test_board_service_pricelist_item_apply_sale_dates(self): + """ + Pricelist item is created to apply on board services at SALE date. + The reservation created take into account the board service + pricelist item created previously according to the SALE date. + """ + # ARRANGE + date_from = fields.date.today() + date_to = fields.date.today() + expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start": datetime.datetime.combine( + date_from, datetime.datetime.min.time() + ), + "date_end": datetime.datetime.combine( + date_to, datetime.datetime.max.time() + ), + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.product1.id, + "board_service_room_type_id": self.board_service_room_type1.id, + "fixed_price": expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertEqual( + reservation_created.service_ids.price_subtotal, + expected_price, + "The reservation created should take into account the board service" + " pricelist item created previously according to the SALE date.", + ) + + @freeze_time("2000-01-01") + def test_board_service_pricelist_item_not_apply_sale_dates(self): + """ + Pricelist item is created to apply on board services at SALE date. + The reservation created DONT take into account the board service pricelist + item created previously according to the SALE date. + """ + # ARRANGE + date_from = fields.date.today() + datetime.timedelta(days=1) + date_to = fields.date.today() + datetime.timedelta(days=1) + not_expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start": datetime.datetime.combine( + date_from, datetime.datetime.min.time() + ), + "date_end": datetime.datetime.combine( + date_to, datetime.datetime.max.time() + ), + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.product1.id, + "board_service_room_type_id": self.board_service_room_type1.id, + "fixed_price": not_expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertNotEqual( + reservation_created.service_ids.price_subtotal, + not_expected_price, + "The reservation created shouldnt take into account the board service pricelist" + " item created previously according to the SALE date.", + ) + + @freeze_time("2000-01-01") + def test_board_service_pricelist_item_apply_consumption_dates(self): + """ + Pricelist item is created to apply on board services + at CONSUMPTION date. + The reservation created take into account the board service + pricelist item created previously according to the CONSUMPTION date. + """ + # ARRANGE + date_from = fields.date.today() + datetime.timedelta(days=1) + date_to = fields.date.today() + datetime.timedelta(days=1) + expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start_consumption": date_from, + "date_end_consumption": date_to, + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.product1.id, + "board_service_room_type_id": self.board_service_room_type1.id, + "fixed_price": expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertEqual( + reservation_created.service_ids.price_subtotal, + expected_price, + "The reservation created should take into account the board service" + " pricelist item created previously according to the CONSUMPTION date.", + ) + + @freeze_time("2000-01-01") + def test_board_service_pricelist_item_not_apply_consumption_dates(self): + """ + Pricelist item is created to apply on board services + at CONSUMPTION date. + The reservation created DONT take into account the board service + pricelist item created previously according to the CONSUMPTION date. + """ + # ARRANGE + date_from = fields.date.today() + datetime.timedelta(days=2) + date_to = fields.date.today() + datetime.timedelta(days=2) + not_expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start": datetime.datetime.combine( + date_from, datetime.datetime.min.time() + ), + "date_end": datetime.datetime.combine( + date_to, datetime.datetime.max.time() + ), + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.product1.id, + "board_service_room_type_id": self.board_service_room_type1.id, + "fixed_price": not_expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertNotEqual( + reservation_created.service_ids.price_subtotal, + not_expected_price, + "The reservation created shouldnt take into account the board service" + " pricelist item created previously according to the CONSUMPTION date.", + ) + + @freeze_time("2000-01-01") + def test_room_type_pricelist_item_apply_sale_dates(self): + """ + Pricelist item is created to apply on room types + at SALE date. + The reservation created take into account the room type + pricelist item created previously according to the SALE date. + """ + # ARRANGE + date_from = fields.date.today() + date_to = fields.date.today() + expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start": datetime.datetime.combine( + date_from, datetime.datetime.min.time() + ), + "date_end": datetime.datetime.combine( + date_to, datetime.datetime.max.time() + ), + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.room_type1.product_id.id, + "fixed_price": expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertEqual( + reservation_created.price_subtotal, + expected_price, + "The reservation created should take into account the room type" + " pricelist item created previously according to the SALE date.", + ) + + @freeze_time("2000-01-01") + def test_room_type_pricelist_item_not_apply_sale_dates(self): + """ + Pricelist item is created to apply on room types + at SALE date. + The reservation created DONT take into account the room type + pricelist item created previously according to the SALE date. + """ + # ARRANGE + date_from = fields.date.today() + datetime.timedelta(days=1) + date_to = fields.date.today() + datetime.timedelta(days=1) + not_expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start": datetime.datetime.combine( + date_from, datetime.datetime.min.time() + ), + "date_end": datetime.datetime.combine( + date_to, datetime.datetime.max.time() + ), + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.room_type1.product_id.id, + "fixed_price": not_expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertNotEqual( + reservation_created.price_subtotal, + not_expected_price, + "The reservation created shouldnt take into account the room type" + " pricelist item created previously according to the SALE date.", + ) + + @freeze_time("2000-01-01") + def test_room_type_pricelist_item_apply_consumption_dates(self): + """ + Pricelist item is created to apply on room types + at CONSUMPTION date. + The reservation created take into account the room type + pricelist item created previously according to the CONSUMPTION date. + """ + # ARRANGE + date_from = fields.date.today() + datetime.timedelta(days=1) + date_to = fields.date.today() + datetime.timedelta(days=1) + expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start_consumption": date_from, + "date_end_consumption": date_to, + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.room_type1.product_id.id, + "fixed_price": expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertEqual( + reservation_created.price_subtotal, + expected_price, + "The reservation created should take into account the room type" + " pricelist item created previously according to the CONSUMPTION date.", + ) + + @freeze_time("2000-01-01") + def test_room_type_pricelist_item_not_apply_consumption_dates(self): + """ + Pricelist item is created to apply on room types + at CONSUMPTION date. + The reservation created DONT take into account the room type + pricelist item created previously according to the CONSUMPTION date. + """ + # ARRANGE + date_from = fields.date.today() + datetime.timedelta(days=2) + date_to = fields.date.today() + datetime.timedelta(days=2) + not_expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start": datetime.datetime.combine( + date_from, datetime.datetime.min.time() + ), + "date_end": datetime.datetime.combine( + date_to, datetime.datetime.max.time() + ), + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.room_type1.product_id.id, + "fixed_price": not_expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertNotEqual( + reservation_created.price_subtotal, + not_expected_price, + "The reservation created shouldnt take into account the room type" + " pricelist item created previously according to the CONSUMPTION date.", + ) + + @freeze_time("2000-01-01") + def test_service_pricelist_item_apply_sale_dates(self): + """ + Pricelist item is created to apply on services at SALE date. + The reservation created take into account the service + pricelist item created previously according to the SALE date. + """ + # ARRANGE + date_from = fields.date.today() + date_to = fields.date.today() + expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start": datetime.datetime.combine( + date_from, datetime.datetime.min.time() + ), + "date_end": datetime.datetime.combine( + date_to, datetime.datetime.max.time() + ), + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.product1.id, + "fixed_price": expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "service_ids": [(0, 0, {"product_id": self.product1.id})], + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertEqual( + reservation_created.service_ids.price_subtotal, + expected_price, + "The reservation created should take into account the service" + " pricelist item created previously according to the SALE date.", + ) + + @freeze_time("2000-01-01") + def test_service_pricelist_item_not_apply_sale_dates(self): + """ + Pricelist item is created to apply on services at SALE date. + The reservation created DONT take into account the service pricelist + item created previously according to the SALE date. + """ + # ARRANGE + date_from = fields.date.today() + datetime.timedelta(days=1) + date_to = fields.date.today() + datetime.timedelta(days=1) + not_expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start": datetime.datetime.combine( + date_from, datetime.datetime.min.time() + ), + "date_end": datetime.datetime.combine( + date_to, datetime.datetime.max.time() + ), + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.product1.id, + "fixed_price": not_expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "service_ids": [(0, 0, {"product_id": self.product1.id})], + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertNotEqual( + reservation_created.service_ids.price_subtotal, + not_expected_price, + "The reservation created shouldnt take into account the service pricelist" + " item created previously according to the SALE date.", + ) + + @freeze_time("2000-01-01") + def test_service_pricelist_item_apply_consumption_dates(self): + """ + Pricelist item is created to apply on services at CONSUMPTION date. + The reservation created take into account the service + pricelist item created previously according to the CONSUMPTION date. + """ + # ARRANGE + date_from = fields.date.today() + datetime.timedelta(days=1) + date_to = fields.date.today() + datetime.timedelta(days=1) + expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start_consumption": date_from, + "date_end_consumption": date_to, + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.product1.id, + "fixed_price": expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "service_ids": [(0, 0, {"product_id": self.product1.id})], + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertEqual( + reservation_created.service_ids.price_subtotal, + expected_price, + "The reservation created should take into account the service" + " pricelist item created previously according to the CONSUMPTION date.", + ) + + @freeze_time("2000-01-01") + def test_service_pricelist_item_not_apply_consumption_dates(self): + """ + Pricelist item is created to apply on services at CONSUMPTION date. + The reservation created DONT take into account the service pricelist + item created previously according to the CONSUMPTION date. + """ + # ARRANGE + date_from = fields.date.today() + datetime.timedelta(days=2) + date_to = fields.date.today() + datetime.timedelta(days=2) + not_expected_price = 1000.0 + vals = { + "pricelist_id": self.pricelist2.id, + "date_start": datetime.datetime.combine( + date_from, datetime.datetime.min.time() + ), + "date_end": datetime.datetime.combine( + date_to, datetime.datetime.max.time() + ), + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.product1.id, + "fixed_price": not_expected_price, + "pms_property_ids": [self.pms_property1.id], + } + self.env["product.pricelist.item"].create(vals) + # ACT + reservation_created = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist2.id, + "service_ids": [(0, 0, {"product_id": self.product1.id})], + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ASSERT + self.assertNotEqual( + reservation_created.service_ids.price_subtotal, + not_expected_price, + "The reservation created shouldnt take into account the service pricelist " + "item created previously according to the CONSUMPTION date.", + ) + + @freeze_time("2000-01-01") + def _test_inconsistencies_pricelist_daily(self): + """ + Test cases to verify that a daily pricelist cannot be created because: + (Test case1): item has two properties and a items daily pricelist only + can has a one property. + (Test case2): item has all properties(pms_property_ids = False indicates + all properties)and a items daily pricelist only can has a one property. + (Test case3): item compute_price is 'percentage' and only can be 'fixed' + for items daily pricelist. + (Test case4): item compute_price is 'percentage' and has two properties + but compute_price can only be fixed and can only have one + property for items daily pricelist. + (Test case5): item compute_price is 'percentage' and has all properties + (pms_property_ids = False indicates all properties)but + compute_pricecan only be fixed and can only have one property for + items daily pricelist. + (Test case6): The difference of days between date_start_consumption and + date_end_consumption is three and the items of a daily pricelist + can only be one. + """ + test_cases = [ + { + "compute_price": "fixed", + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.today() + + datetime.timedelta(days=1), + }, + { + "compute_price": "fixed", + "pms_property_ids": False, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.today() + + datetime.timedelta(days=1), + }, + { + "compute_price": "percentage", + "pms_property_ids": [self.pms_property1.id], + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.today() + + datetime.timedelta(days=1), + }, + { + "compute_price": "percentage", + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.today() + + datetime.timedelta(days=1), + }, + { + "compute_price": "percentage", + "pms_property_ids": False, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.today() + + datetime.timedelta(days=1), + }, + { + "compute_price": "fixed", + "pms_property_ids": [self.pms_property1.id], + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.today() + + datetime.timedelta(days=3), + }, + ] + + for tc in test_cases: + with self.subTest(k=tc): + with self.assertRaises( + ValidationError, + msg="Item only can has one property, the compute price only can" + "be fixed and the difference between date_start_consumption" + "and date_end_consumption only can be 1", + ): + self.room_type1.pms_property_ids = tc["pms_property_ids"] + item = self.env["product.pricelist.item"].create( + { + "pms_property_ids": tc["pms_property_ids"], + "compute_price": tc["compute_price"], + "applied_on": "0_product_variant", + "product_id": self.room_type1.product_id.id, + "date_start_consumption": tc["date_start_consumption"], + "date_end_consumption": tc["date_end_consumption"], + } + ) + self.pricelist_test = self.env["product.pricelist"].create( + { + "name": "Pricelist test", + "pricelist_type": "daily", + "pms_property_ids": tc["pms_property_ids"], + "item_ids": [item.id], + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + + @freeze_time("2020-01-01") + def test_consistency_pricelist_daily(self): + """ + Test to verify that a daily pricelist is created. + Create a pricelist item with a property, the value of compute_price is + fixed and date_start_consumption date_end_consumption has the same value + """ + self.room_type1.pms_property_ids = (self.pms_property1.id,) + item = self.env["product.pricelist.item"].create( + { + "pms_property_ids": [self.pms_property1.id], + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": self.room_type1.product_id.id, + "date_start_consumption": datetime.date.today(), + "date_end_consumption": datetime.date.today(), + } + ) + self.pricelist_test = self.env["product.pricelist"].create( + { + "name": "Pricelist test", + "pricelist_type": "daily", + "pms_property_ids": [self.pms_property1.id], + "item_ids": [item.id], + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + self.assertTrue(self.pricelist_test, "Pricelist not created.") + + @freeze_time("2000-01-01") + def test_simple_price_without_items(self): + """ + Test case for no items applied in a reservation. + """ + + # ARRANGE + self.room_type = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "name": "Single", + "default_code": "S", + "class_id": self.room_type_class1.id, + "list_price": 30, + } + ) + + self.room = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "Single 1", + "room_type_id": self.room_type.id, + } + ) + reservation = self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.today(), + "checkout": datetime.datetime.today() + datetime.timedelta(days=3), + "preferred_room_id": self.room.id, + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + # ACT + n_days = (reservation.checkout - reservation.checkin).days + expected_price = self.room.room_type_id.list_price * n_days + + # ASSERT + self.assertEqual( + expected_price, reservation.price_subtotal, "The price is not as expected" + ) + + @freeze_time("2022-01-01") + def test_items_sort(self): + """ + Test cases to verify the order for each field considered individually + Test cases to prioritize fields over other fields: + 1. applied_on + 2. date + 3. date consumption + 4. num. properties + 5. id + - tie + - no [date_start|date_end|date_start_consumption|date_end_consumption] + """ + # ARRANGE + self.product_category = self.env["product.category"].create( + {"name": "Category1"} + ) + self.product_template = self.env["product.template"].create( + {"name": "Template1"} + ) + self.room_type = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id, self.pms_property2.id], + "name": "Single", + "default_code": "SGL", + "class_id": self.room_type_class1.id, + "list_price": 30, + } + ) + + self.room = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "101", + "room_type_id": self.room_type.id, + } + ) + properties = self.room_type.product_id.pms_property_ids.ids + test_cases = [ + { + "name": "sorting applied_on", + "expected_price": 50 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "2_product_category", + "categ_id": self.product_category.id, + "product_id": self.room_type.product_id.id, + "fixed_price": 60.0, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "fixed_price": 50.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "1_product", + "product_id": self.room_type.product_id.id, + "product_tmpl_id": self.product_template.id, + "fixed_price": 40.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "sorting SALE date min range", + "expected_price": 50.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start": datetime.datetime.now(), + "date_end": datetime.datetime.now() + + datetime.timedelta(days=2), + "fixed_price": 60.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start": datetime.datetime.now(), + "date_end": datetime.datetime.now() + + datetime.timedelta(days=1), + "fixed_price": 50.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start": datetime.datetime.now(), + "date_end": datetime.datetime.now() + + datetime.timedelta(days=3), + "fixed_price": 40.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "sorting CONSUMPTION date min range", + "expected_price": 40.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=6), + "fixed_price": 60.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=10), + "fixed_price": 50.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=3), + "fixed_price": 40.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "sorting num. properties", + "expected_price": 50.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "fixed_price": 60.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "pms_property_ids": [self.pms_property1.id], + "fixed_price": 50.0, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "pms_property_ids": [ + self.pms_property1.id, + self.pms_property2.id, + ], + "fixed_price": 40.0, + }, + ], + }, + { + "name": "sorting by item id", + "expected_price": 40.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "fixed_price": 60.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "fixed_price": 50.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "fixed_price": 40.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "prioritize applied_on over SALE date", + "expected_price": 60.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start": datetime.datetime.now(), + "date_end": datetime.datetime.now() + + datetime.timedelta(days=2), + "fixed_price": 60.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "product_id": self.room_type.product_id.id, + "product_tmpl_id": self.product_template.id, + "applied_on": "1_product", + "date_start": datetime.datetime.now(), + "date_end": datetime.datetime.now() + + datetime.timedelta(days=1), + "fixed_price": 50.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "prioritize SALE date over CONSUMPTION date", + "expected_price": 120.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start": datetime.datetime.now(), + "date_end": datetime.datetime.now() + + datetime.timedelta(days=10), + "fixed_price": 120.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=3), + "fixed_price": 50.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "prioritize CONSUMPTION date over min. num. properties", + "expected_price": 50.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=3), + "fixed_price": 120.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=3), + "pms_property_ids": [ + self.pms_property1.id, + self.pms_property2.id, + ], + "fixed_price": 50.0, + }, + ], + }, + { + "name": "prioritize min. num. properties over item id", + "expected_price": 50.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=3), + "fixed_price": 120.0, + "pms_property_ids": properties, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=3), + "pms_property_ids": [ + self.pms_property1.id, + self.pms_property2.id, + ], + "fixed_price": 50.0, + }, + ], + }, + { + "name": "tie => order by item id", + "expected_price": 50.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=3), + "date_start": datetime.datetime.now(), + "date_end": datetime.datetime.now() + + datetime.timedelta(days=3), + "pms_property_ids": [ + self.pms_property1.id, + self.pms_property2.id, + ], + "fixed_price": 120.0, + }, + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now() + + datetime.timedelta(days=3), + "date_start": datetime.datetime.now(), + "date_end": datetime.datetime.now() + + datetime.timedelta(days=3), + "pms_property_ids": [ + self.pms_property1.id, + self.pms_property2.id, + ], + "fixed_price": 50.0, + }, + ], + }, + { + "name": "no SALE DATE START", + "expected_price": 40.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_end": datetime.datetime.now() + + datetime.timedelta(days=1), + "fixed_price": 40.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "no SALE DATE END", + "expected_price": 40.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start": datetime.datetime.now(), + "fixed_price": 40.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "no consumption DATE START", + "expected_price": 40.0 + self.room_type.list_price * 2, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_end_consumption": datetime.datetime.now(), + "fixed_price": 40.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "no consumption DATE END", + "expected_price": 40.0 * 3, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "fixed_price": 40.0, + "pms_property_ids": properties, + }, + ], + }, + { + "name": "only applied consumption in one night", + "expected_price": 40.0 + self.room_type.list_price * 2, + "items": [ + { + "pricelist_id": self.pricelist1.id, + "applied_on": "0_product_variant", + "product_id": self.room_type.product_id.id, + "date_start_consumption": datetime.datetime.now(), + "date_end_consumption": datetime.datetime.now(), + "fixed_price": 40.0, + "pms_property_ids": properties, + }, + ], + }, + ] + + for tc in test_cases: + with self.subTest(k=tc): + + # ARRANGE + items = [] + for item in tc["items"]: + item = self.env["product.pricelist.item"].create(item) + items.append(item.id) + + # ACT + reservation = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + + datetime.timedelta(days=3), + "preferred_room_id": self.room.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + reservation_price = reservation.price_subtotal + self.env["pms.reservation"].browse(reservation.id).unlink() + self.env["product.pricelist.item"].browse(items).unlink() + + # ASSERT + self.assertEqual(tc["expected_price"], reservation_price, tc["name"]) diff --git a/pms/tests/test_pms_pricelist_settings.py b/pms/tests/test_pms_pricelist_settings.py new file mode 100644 index 0000000000..7b04ebc96a --- /dev/null +++ b/pms/tests/test_pms_pricelist_settings.py @@ -0,0 +1,55 @@ +from odoo.exceptions import ValidationError + +from .common import TestPms + + +class TestPmsPricelistSettings(TestPms): + def test_advanced_pricelist_exists(self): + """ + Check if value of Pricelist parameter in sales settings is Advanced Price Rules. + Find the value of Pricelist parameter + with the key product.product_pricelist_setting and check if is equal to "advanced". + """ + # ARRANGE + key = "product.product_pricelist_setting" + value = "advanced" + + # ACT + found_value = self.env["ir.config_parameter"].sudo().get_param(key) + + # ASSERT + self.assertEqual( + found_value, value, "Parameter of Pricelist in setting is not 'advanced'" + ) + + def test_product_pricelist_setting_not_modified(self): + """ + Check that Pricelist parameter 'advanced' cannot be modified. + Set the value of product.product_pricelist_setting to 'basic' + but is not possible because this only can be 'advanced'. + """ + # ARRANGE + key = "product.product_pricelist_setting" + value = "basic" + + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="The Pricelist parameter 'advanced' was modified." + ): + self.env["ir.config_parameter"].set_param(key, value) + + def test_product_pricelist_setting_not_unlink(self): + """ + Check that Pricelist parameter 'advanced' cannot be unlink. + Try to unlink the parameter product_pricelist with value 'advanced' + but this should be impossible. + """ + # ARRANGE + key = "product.product_pricelist_setting" + value = "advanced" + + # ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.env["ir.config_parameter"].search( + [("key", "=", key), ("value", "=", value)] + ).unlink() diff --git a/pms/tests/test_pms_res_users.py b/pms/tests/test_pms_res_users.py new file mode 100644 index 0000000000..12fb0b69a4 --- /dev/null +++ b/pms/tests/test_pms_res_users.py @@ -0,0 +1,165 @@ +from odoo.exceptions import ValidationError + +from .common import TestPms + + +class TestPmsResUser(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + # create a company and properties + cls.company_A = cls.env["res.company"].create( + { + "name": "Pms_Company1", + } + ) + cls.company_B = cls.env["res.company"].create( + { + "name": "Pms_Company2", + } + ) + cls.property_A1 = cls.env["pms.property"].create( + { + "name": "Pms_property", + "company_id": cls.company_A.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + cls.property_A2 = cls.env["pms.property"].create( + { + "name": "Pms_property2", + "company_id": cls.company_A.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + cls.property_B1 = cls.env["pms.property"].create( + { + "name": "Pms_propertyB1", + "company_id": cls.company_B.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + + def test_property_not_in_allowed_properties(self): + """ + Property not allowed for the user + Check a user cannot have an active property + that is not in the allowed properties + + Company_A ---> Property_A1, Property_A2 + Company_B ---> Property_B1 + + + """ + # ARRANGE + Users = self.env["res.users"] + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Some property is not included in the allowed properties", + ): + Users.create( + { + "name": "Test User", + "login": "test_user", + "company_ids": [(4, self.company_A.id)], + "company_id": self.company_A.id, + "pms_property_ids": [(4, self.property_A1.id)], + "pms_property_id": self.property_B1.id, + } + ) + + def test_property_not_in_allowed_companies(self): + """ + Property not allowed for the user + Check a user cannot have a property in allowed properties + that does not belong to their companies + + Company_A ---> Property_A1, Property_A2 + Company_B ---> Property_B1 + + """ + # ARRANGE + Users = self.env["res.users"] + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="Some property doesn't belong to the allowed companies" + ): + Users.create( + { + "name": "Test User", + "login": "test_user", + "company_ids": [(4, self.company_A.id)], + "company_id": self.company_A.id, + "pms_property_ids": [ + (4, self.property_A1.id), + (4, self.property_B1.id), + ], + "pms_property_id": self.property_A1.id, + } + ) + + def test_property_in_allowed_properties(self): + """ + Successful user creation + Check user creation with active property in allowed properties + + Company_A ---> Property_A1, Property_A2 + Company_B ---> Property_B1 + + """ + # ARRANGE + Users = self.env["res.users"] + # ACT + user1 = Users.create( + { + "name": "Test User", + "login": "test_user", + "company_ids": [(4, self.company_A.id)], + "company_id": self.company_A.id, + "pms_property_ids": [ + (4, self.property_A1.id), + (4, self.property_A2.id), + ], + "pms_property_id": self.property_A1.id, + } + ) + # ASSERT + self.assertIn( + user1.pms_property_id, + user1.pms_property_ids, + "Active property not in allowed properties", + ) + + def test_properties_belong_to_companies(self): + """ + Successful user creation + Check user creation with active property and allowed properties + belonging to the allowed companies + + Company_A ---> Property_A1, Property_A2 + Company_B ---> Property_B1 + + """ + # ARRANGE + Users = self.env["res.users"] + # ACT + user1 = Users.create( + { + "name": "Test User", + "login": "test_user", + "company_ids": [(4, self.company_A.id)], + "company_id": self.company_A.id, + "pms_property_ids": [ + (4, self.property_A1.id), + (4, self.property_A2.id), + ], + "pms_property_id": self.property_A1.id, + } + ) + # ASSERT + self.assertEqual( + user1.pms_property_id.company_id, + user1.company_id, + "Active property doesn't belong to active company", + ) diff --git a/pms/tests/test_pms_reservation.py b/pms/tests/test_pms_reservation.py new file mode 100644 index 0000000000..9255ca3ea4 --- /dev/null +++ b/pms/tests/test_pms_reservation.py @@ -0,0 +1,3426 @@ +import datetime + +from freezegun import freeze_time + +from odoo import fields +from odoo.exceptions import UserError, ValidationError + +from .common import TestPms + + +class TestPmsReservations(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + # create a room type availability + cls.room_type_availability = cls.env["pms.availability.plan"].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [cls.pricelist1.id])], + } + ) + + # create room type + cls.room_type_double = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + } + ) + + cls.room_type_triple = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Triple Test", + "default_code": "TRP_Test", + "class_id": cls.room_type_class1.id, + } + ) + + # create rooms + cls.room1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 101", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + "extra_beds_allowed": 1, + } + ) + + cls.room2 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 102", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + "extra_beds_allowed": 1, + } + ) + + cls.room3 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 103", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + "extra_beds_allowed": 1, + } + ) + + cls.room4 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Triple 104", + "room_type_id": cls.room_type_triple.id, + "capacity": 3, + "extra_beds_allowed": 1, + } + ) + cls.partner1 = cls.env["res.partner"].create( + { + "firstname": "Jaime", + "lastname": "García", + "email": "jaime@example.com", + "birthdate_date": "1983-03-01", + "gender": "male", + } + ) + cls.id_category = cls.env["res.partner.id_category"].create( + {"name": "DNI", "code": "D"} + ) + cls.sale_channel_direct = cls.env["pms.sale.channel"].create( + {"name": "sale channel direct", "channel_type": "direct"} + ) + cls.sale_channel1 = cls.env["pms.sale.channel"].create( + {"name": "saleChannel1", "channel_type": "indirect"} + ) + cls.agency1 = cls.env["res.partner"].create( + { + "firstname": "partner1", + "is_agency": True, + "invoice_to_agency": "always", + "default_commission": 15, + "sale_channel_id": cls.sale_channel1.id, + } + ) + + @freeze_time("2012-01-14") + def test_reservation_dates_not_consecutive(self): + """ + Check the constrain if not consecutive dates + ---------------- + Create correct reservation set 3 reservation lines consecutives (nights) + """ + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + three_days_later = fields.date.today() + datetime.timedelta(days=3) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Error, it has been allowed to create a reservation with non-consecutive days", + ): + self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "reservation_line_ids": [ + (0, False, {"date": today}), + (0, False, {"date": tomorrow}), + (0, False, {"date": three_days_later}), + ], + } + ) + + @freeze_time("2012-01-14") + def test_reservation_dates_compute_checkin_out(self): + """ + Check the reservation creation with specific reservation lines + anc compute checkin checkout + ---------------- + Create reservation with correct reservation lines and check + the checkin and checkout fields. Take into account that the + checkout of the reservation must be the day after the last night + (view checkout assertEqual) + """ + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + two_days_later = fields.date.today() + datetime.timedelta(days=2) + + # ACT + reservation = self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + "reservation_line_ids": [ + (0, False, {"date": today}), + (0, False, {"date": tomorrow}), + (0, False, {"date": two_days_later}), + ], + } + ) + + # ASSERT + self.assertEqual( + reservation.checkin, + today, + "The calculated checkin of the reservation does \ + not correspond to the first day indicated in the dates", + ) + self.assertEqual( + reservation.checkout, + two_days_later + datetime.timedelta(days=1), + "The calculated checkout of the reservation does \ + not correspond to the last day indicated in the dates", + ) + + @freeze_time("2012-01-14") + def test_create_reservation_start_date(self): + """ + Check that the reservation checkin and the first reservation date are equal. + ---------------- + Create a reservation and check if the first reservation line date are the same + date that the checkin date. + """ + # reservation should start on checkin day + + # ARRANGE + today = fields.date.today() + checkin = today + datetime.timedelta(days=8) + checkout = checkin + datetime.timedelta(days=11) + reservation_vals = { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + + # ACT + reservation = self.env["pms.reservation"].create(reservation_vals) + + self.assertEqual( + reservation.reservation_line_ids[0].date, + checkin, + "Reservation lines don't start in the correct date", + ) + + @freeze_time("2012-01-14") + def test_create_reservation_end_date(self): + """ + Check that the reservation checkout and the last reservation date are equal. + ---------------- + Create a reservation and check if the last reservation line date are the same + date that the checkout date. + """ + # ARRANGE + today = fields.date.today() + checkin = today + datetime.timedelta(days=8) + checkout = checkin + datetime.timedelta(days=11) + reservation_vals = { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + + # ACT + reservation = self.env["pms.reservation"].create(reservation_vals) + + self.assertEqual( + reservation.reservation_line_ids[-1].date, + checkout - datetime.timedelta(1), + "Reservation lines don't end in the correct date", + ) + + @freeze_time("2012-01-14") + def test_split_reservation01(self): + """ + # TEST CASE + The reservation shouldn't be splitted + preferred_room_id with availability provided + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | test | test | test | | | | + | Double 102 | | | | | | | + | Double 103 | | | | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + # ACT + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r_test.flush() + + # ASSERT + self.assertTrue( + all( + elem.room_id.id == r_test.reservation_line_ids[0].room_id.id + for elem in r_test.reservation_line_ids + ), + "The entire reservation should be allocated in the preferred room", + ) + + @freeze_time("2012-01-14") + def test_split_reservation02(self): + """ + # TEST CASE + The reservation shouldn't be splitted + room_type_id with availability provided + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | test | test | test | | | | + | Double 102 | | | | | | | + | Double 103 | | | | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + # ACT + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r_test.flush() + + # ASSERT + self.assertFalse(r_test.splitted, "The reservation shouldn't be splitted") + + def test_split_reservation03(self): + """ + # TEST CASE + The reservation should be splitted in 2 rooms + (there is only one better option on day 02 and a draw the next day. + The night before should be prioritized) + +------------+------+------+------+------+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+------+----+----+ + | Double 101 | test | r3 | | | | | + | Double 102 | r1 | test | test | test | | | + | Double 103 | r2 | r4 | | | | | + +------------+------+------+------+------+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r1.reservation_line_ids[0].room_id = self.room2.id + r1.flush() + + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r2.reservation_line_ids[0].room_id = self.room3.id + r2.flush() + + r3 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r3.reservation_line_ids[0].room_id = self.room1.id + r3.flush() + + r4 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r4.reservation_line_ids[0].room_id = self.room3.id + r4.flush() + expected_num_changes = 2 + + # ACT + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=4), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r_test.flush() + # ASSERT + self.assertEqual( + expected_num_changes, + len(r_test.reservation_line_ids.mapped("room_id")), + "The reservation shouldn't have more than 2 changes", + ) + + @freeze_time("2012-01-14") + def test_split_reservation04(self): + """ + # TEST CASE + The reservation should be splitted in 3 rooms + (there are 2 best options on day 03 and room of last night is not available) + +------------+------+------+------+------+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+------+----+----+ + | Double 101 | test | r3 | test | test | | | + | Double 102 | r1 | test | r5 | | | | + | Double 103 | r2 | r4 | | | | | + +------------+------+------+------+------+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r1.reservation_line_ids[0].room_id = self.room2.id + r1.flush() + + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r2.reservation_line_ids[0].room_id = self.room3.id + r2.flush() + + r3 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r3.reservation_line_ids[0].room_id = self.room1.id + r3.flush() + + r4 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r4.reservation_line_ids[0].room_id = self.room3.id + r4.flush() + + r5 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=2), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r5.reservation_line_ids[0].room_id = self.room2.id + r5.flush() + + # ACT + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=4), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r_test.flush() + + rooms = 0 + last_room = None + for line in r_test.reservation_line_ids: + if line.room_id != last_room: + last_room = line.room_id + rooms += 1 + + # ASSERT + self.assertEqual( + 3, rooms, "The reservation shouldn't be splitted in more than 3 roomss" + ) + + @freeze_time("2012-01-14") + def test_split_reservation05(self): + """ + # TEST CASE + The preferred room_id is not available + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 |r1/tst| | | | | | + | Double 102 | | | | | | | + | Double 103 | | | | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r1.reservation_line_ids[0].room_id = self.room1 + r1.flush() + + # ACT & ASSERT + with self.assertRaises(ValidationError): + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + } + ) + r_test.flush() + + @freeze_time("2012-01-14") + def test_split_reservation06(self): + """ + # TEST CASE + There's no availability in the preferred_room_id provided + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 |r1/tst| tst | | | | + | Double 102 | | | | | | | + | Double 103 | | | | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r1.reservation_line_ids[0].room_id = self.room1 + r1.reservation_line_ids[1].room_id = self.room1 + r1.flush() + + # ACT & ASSERT + with self.assertRaises(ValidationError): + r_test = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + } + ) + r_test.flush() + + @freeze_time("2012-01-14") + def test_split_reservation07(self): + """ + # TEST CASE + There's no availability + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 | r1 | r1 | | | | + | Double 102 | r2 | r2 | r2 | | | | + | Double 103 | r3 | r3 | r3 | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r1.reservation_line_ids[0].room_id = self.room1 + r1.reservation_line_ids[1].room_id = self.room1 + r1.reservation_line_ids[2].room_id = self.room1 + r1.flush() + + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r2.reservation_line_ids[0].room_id = self.room2 + r2.reservation_line_ids[1].room_id = self.room2 + r2.reservation_line_ids[2].room_id = self.room2 + r2.flush() + + r3 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r3.reservation_line_ids[0].room_id = self.room3 + r3.reservation_line_ids[1].room_id = self.room3 + r3.reservation_line_ids[2].room_id = self.room3 + r3.flush() + + # ACT & ASSERT + with self.assertRaises(ValidationError): + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + } + ) + + @freeze_time("2012-01-14") + def test_manage_children_raise(self): + # TEST CASE + """ + Check if the error occurs when trying to put more people than the capacity of the room. + -------------- + Create a reservation with a double room whose capacity is two and try to create + it with two adults and a child occupying the room. + """ + # NO ARRANGE + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="The number of people is greater than the capacity of the room", + ): + reservation = self.env["pms.reservation"].create( + { + "adults": 2, + "children_occupying": 1, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + } + ) + reservation.flush() + + def test_reservation_action_assign(self): + """ + Checks the correct operation of the assign method + --------------- + Create a new reservation with only room_type(autoassign -> to_assign = True), + and the we call to action_assign method to confirm the assignation + """ + res = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ACT + res.action_assign() + # ASSERT + self.assertFalse(res.to_assign, "The reservation should be marked as assigned") + + def test_reservation_auto_assign_on_create(self): + """ + When creating a reservation with a specific room, + it is not necessary to mark it as to be assigned + --------------- + Create a new reservation with specific preferred_room_id, + "to_assign" should be set to false automatically + """ + # ARRANGE + + # ACT + res = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + # ASSERT + self.assertFalse( + res.to_assign, "Reservation with preferred_room_id set to to_assign = True" + ) + + def test_reservation_auto_assign_after_create(self): + """ + When assigning a room manually to a reservation marked "to be assigned", + this field should be automatically unchecked + --------------- + Create a new reservation without preferred_room_id (only room_type), + "to_assign" is True, then set preferred_room_id and "to_assign" should + be set to false automatically + """ + # ARRANGE + # set the priority of the rooms to control the room chosen by auto assign + self.room1.sequence = 1 + self.room2.sequence = 2 + + res = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + # ACT + # res shoul be room1 in preferred_room_id (minor sequence) + res.preferred_room_id = self.room2.id + + # ASSERT + self.assertFalse( + res.to_assign, + "The reservation should be marked as assigned automatically \ + when assigning the specific room", + ) + + def test_reservation_to_assign_on_create(self): + """ + Check the reservation action assign. + Create a reservation and change the reservation to 'to_assign' = False + through action_assign() method + """ + # ARRANGE + res = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ACT + res.action_assign() + # ASSERT + self.assertFalse(res.to_assign, "The reservation should be marked as assigned") + + def test_reservation_action_cancel(self): + """ + Check if the reservation has been cancelled correctly. + ------------- + Create a reservation and change his state to cancelled + through the action_cancel() method. + """ + # ARRANGE + res = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ACT + res.action_cancel() + # ASSERT + self.assertEqual(res.state, "cancel", "The reservation should be cancelled") + + @freeze_time("1981-11-01") + def test_reservation_action_checkout(self): + # TEST CASE + """ + Check that when the date of a reservation passes, it goes to the 'done' status. + ------------- + Create a host, a reservation and a check-in partner. Assign the partner and the + reservation to the check-in partner and after one day of the reservation it + must be in the 'done' status + """ + # ARRANGE + host = self.env["res.partner"].create( + { + "firstname": "Miguel", + "mobile": "654667733", + "email": "miguel@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "30065089H", + "valid_from": datetime.date.today(), + "partner_id": host.id, + "country_id": self.env.ref("base.es").id, + } + ) + r1 = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "room_type_id": self.room_type_double.id, + "partner_id": host.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r1.flush() + checkin = self.env["pms.checkin.partner"].create( + { + "partner_id": host.id, + "reservation_id": r1.id, + } + ) + checkin.action_on_board() + checkin.flush() + + # ACT + with freeze_time("1981-11-02"): + r1._cache.clear() + r1.action_reservation_checkout() + + # ASSERT + self.assertEqual( + r1.state, "done", "The reservation status should be done after checkout." + ) + + def _test_check_date_order(self): + """ + Check that the date order of a reservation is correct. + --------------- + Create a reservation with today's date and then check that the date order is also today + """ + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "partner_id": self.partner1.id, + "room_type_id": self.room_type_double.id, + } + ) + + date_order = reservation.date_order + date_order_expected = datetime.datetime( + date_order.year, + date_order.month, + date_order.day, + date_order.hour, + date_order.minute, + date_order.second, + ) + + reservation.flush() + self.assertEqual( + date_order, + date_order_expected, + "Date Order isn't correct", + ) + + def test_check_checkin_datetime(self): + """ + Check that the checkin datetime of a reservation is correct. + ------------------ + Create a reservation and then check if the checkin datetime + it is correct + """ + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": fields.date.today() + datetime.timedelta(days=300), + "checkout": fields.date.today() + datetime.timedelta(days=305), + "partner_id": self.partner1.id, + "room_type_id": self.room_type_double.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + r = reservation.checkin + checkin_expected = datetime.datetime(r.year, r.month, r.day, 14, 00) + checkin_expected = self.pms_property1.date_property_timezone(checkin_expected) + + self.assertEqual( + str(reservation.checkin_datetime), + str(checkin_expected), + "Date Order isn't correct", + ) + + def test_check_allowed_room_ids(self): + """ + Check available rooms after creating a reservation. + ----------- + Create an availability rule, create a reservation, + and then check that the allowed_room_ids field filtered by room + type of the reservation and the room_type_id.room_ids field of the + availability rule match. + """ + availability_rule = self.env["pms.availability.plan.rule"].create( + { + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type_double.id, + "availability_plan_id": self.room_type_availability.id, + "date": fields.date.today() + datetime.timedelta(days=153), + } + ) + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": fields.date.today() + datetime.timedelta(days=150), + "checkout": fields.date.today() + datetime.timedelta(days=152), + "partner_id": self.partner1.id, + "room_type_id": self.room_type_double.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.assertEqual( + reservation.allowed_room_ids.filtered( + lambda r: r.room_type_id.id == availability_rule.room_type_id.id + ).ids, + availability_rule.room_type_id.room_ids.ids, + "Rooms allowed don't match", + ) + + def test_partner_is_agency(self): + """ + Check that a reservation created with an agency and without a partner + assigns that agency as a partner. + ------------- + Create an agency and then create a reservation to which that agency + assigns but does not associate any partner. + Then check that the partner of that reservation is the same as the agency + """ + sale_channel1 = self.env["pms.sale.channel"].create( + {"name": "Test Indirect", "channel_type": "indirect"} + ) + agency = self.env["res.partner"].create( + { + "firstname": "partner1", + "is_agency": True, + "sale_channel_id": sale_channel1.id, + "invoice_to_agency": "always", + } + ) + + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": fields.date.today() + datetime.timedelta(days=150), + "checkout": fields.date.today() + datetime.timedelta(days=152), + "agency_id": agency.id, + "room_type_id": self.room_type_double.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + reservation.flush() + + self.assertEqual( + reservation.partner_id.id, + agency.id, + "Partner_id doesn't match with agency_id", + ) + + def test_agency_pricelist(self): + """ + Check that a pricelist of a reservation created with an + agency and without a partner and the pricelist of that + agency are the same. + ------------- + Create an agency with field apply_pricelist is True and + then create a reservation to which that agency + assigns but does not associate any partner. + Then check that the pricelist of that reservation is the same as the agency + """ + sale_channel1 = self.env["pms.sale.channel"].create( + { + "name": "Test Indirect", + "channel_type": "indirect", + "product_pricelist_ids": [(6, 0, [self.pricelist1.id])], + } + ) + agency = self.env["res.partner"].create( + { + "firstname": "partner1", + "is_agency": True, + "sale_channel_id": sale_channel1.id, + "apply_pricelist": True, + } + ) + + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": fields.date.today() + datetime.timedelta(days=150), + "checkout": fields.date.today() + datetime.timedelta(days=152), + "agency_id": agency.id, + "room_type_id": self.room_type_double.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.assertEqual( + reservation.pricelist_id.id, + reservation.agency_id.property_product_pricelist.id, + "Rervation pricelist doesn't match with Agency pricelist", + ) + + def test_compute_access_url(self): + """ + Check that the access_url field of the reservation is created with a correct value. + ------------- + Create a reservation and then check that the access_url field has the value + my/reservation/(reservation.id) + """ + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": fields.date.today() + datetime.timedelta(days=150), + "checkout": fields.date.today() + datetime.timedelta(days=152), + "partner_id": self.partner1.id, + "room_type_id": self.room_type_double.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + url = "/my/reservations/%s" % reservation.id + self.assertEqual(reservation.access_url, url, "Reservation url isn't correct") + + @freeze_time("2012-01-14") + def test_compute_ready_for_checkin(self): + """ + Check that the ready_for_checkin field is True when the reservation + checkin day is today. + --------------- + Create two hosts, create a reservation with a checkin date today, + and associate two checkin partners with that reservation and with + each of the hosts. + Then check that the ready_for_checkin field of the reservation is True + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Miguel", + "mobile": "654667733", + "email": "miguel@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "30065000H", + "valid_from": datetime.date.today(), + "partner_id": self.host1.id, + "country_id": self.env.ref("base.es").id, + } + ) + self.host2 = self.env["res.partner"].create( + { + "firstname": "Brais", + "mobile": "654437733", + "email": "brais@example.com", + "birthdate_date": "1995-12-10", + "gender": "male", + } + ) + self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "30065089H", + "valid_from": datetime.date.today(), + "partner_id": self.host2.id, + "country_id": self.env.ref("base.es").id, + } + ) + self.reservation = self.env["pms.reservation"].create( + { + "checkin": "2012-01-14", + "checkout": "2012-01-17", + "partner_id": self.host1.id, + "allowed_checkin": True, + "pms_property_id": self.pms_property1.id, + "adults": 3, + "room_type_id": self.room_type_triple.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.checkin1 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host1.id, + "reservation_id": self.reservation.id, + } + ) + self.checkin2 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host2.id, + "reservation_id": self.reservation.id, + } + ) + + self.reservation.checkin_partner_ids = [ + (6, 0, [self.checkin1.id, self.checkin2.id]) + ] + self.assertTrue( + self.reservation.ready_for_checkin, + "Reservation should is ready for checkin", + ) + + def test_check_checkout_less_checkin(self): + """ + Check that a reservation cannot be created with the + checkin date greater than the checkout date + --------------- + Create a reservation with the checkin date 3 days + after the checkout date, this should throw an error. + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + with self.assertRaises(UserError): + self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=3), + "checkout": fields.date.today(), + "pms_property_id": self.pms_property1.id, + "partner_id": self.host1.id, + "room_type_id": self.room_type_double.id, + } + ) + + @freeze_time("2012-01-14") + def test_check_more_adults_than_beds(self): + """ + Check that a reservation cannot be created when the field + adults is greater than the capacity of the room. + ------------- + Try to create a reservation with a double room and the + field 'adults'=4, this should throw a mistake because the + room capacity is lesser than the number of adults. + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + with self.assertRaises(ValidationError): + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "pms_property_id": self.pms_property1.id, + "partner_id": self.host1.id, + "preferred_room_id": self.room1.id, + "adults": 4, + } + ) + reservation.flush() + + @freeze_time("2012-01-14") + def test_check_format_arrival_hour(self): + """ + Check that the format of the arrival_hour field is correct(HH:mm) + ------------- + Create a reservation with the wrong arrival hour date + format (HH:mm:ss), this should throw an error. + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + with self.assertRaises(ValidationError): + self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "pms_property_id": self.pms_property1.id, + "partner_id": self.host1.id, + "arrival_hour": "14:00:00", + } + ) + + @freeze_time("2012-01-14") + def test_check_format_departure_hour(self): + """ + Check that the format of the departure_hour field is correct(HH:mm) + ------------- + Create a reservation with the wrong departure hour date + format (HH:mm:ss), this should throw an error. + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + with self.assertRaises(ValidationError): + self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "pms_property_id": self.pms_property1.id, + "partner_id": self.host1.id, + "departure_hour": "14:00:00", + } + ) + + @freeze_time("2012-01-14") + def test_check_property_integrity_room(self): + """ + Check that a reservation cannot be created with a room + of a different property. + ------------ + Try to create a reservation for property2 with a + preferred_room that belongs to property1, this + should throw an error . + """ + self.property2 = self.env["pms.property"].create( + { + "name": "MY PMS TEST", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + self.room_type_double.pms_property_ids = [ + (6, 0, [self.pms_property1.id, self.property2.id]) + ] + with self.assertRaises(ValidationError): + self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "pms_property_id": self.property2.id, + "partner_id": self.host1.id, + "room_type_id": self.room_type_double.id, + "preferred_room_id": self.room1.id, + } + ) + + @freeze_time("2012-01-14") + def test_shared_folio_true(self): + """ + Check that the shared_folio field of a reservation whose + folio has other reservations is True. + --------- + Create a reservation and then create another reservation with + its folio_id = folio_id of the previous reservation. This + should set shared_folio to True + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=60), + "checkout": fields.date.today() + datetime.timedelta(days=65), + "pms_property_id": self.pms_property1.id, + "partner_id": self.host1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.reservation2 = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=60), + "checkout": fields.date.today() + datetime.timedelta(days=64), + "pms_property_id": self.pms_property1.id, + "partner_id": self.host1.id, + "folio_id": self.reservation.folio_id.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.assertTrue( + self.reservation.shared_folio, + "Folio.reservations > 1, so reservation.shared_folio must be True", + ) + + @freeze_time("2012-01-14") + def test_shared_folio_false(self): + """ + Check that the shared_folio field for a reservation whose folio has no + other reservations is False. + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=60), + "checkout": fields.date.today() + datetime.timedelta(days=65), + "pms_property_id": self.pms_property1.id, + "partner_id": self.host1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.assertFalse( + self.reservation.shared_folio, + "Folio.reservations = 1, so reservation.shared_folio must be False", + ) + + @freeze_time("2012-01-14") + def test_reservation_action_cancel_fail(self): + """ + Check that a reservation cannot be in the cancel state if + the cancellation is not allowed. + --------- + Create a reservation, put its state = "canceled" and then try to + pass its state to cancel using the action_cancel () method. This + should throw an error because a reservation with state cancel cannot + be canceled again. + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "room_type_id": self.room_type_double.id, + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + with self.assertRaises(ValidationError): + reservation.state = "cancel" + + @freeze_time("2012-01-14") + def test_cancelation_reason_noshow(self): + """ + Check that if a reservation has already passed and there is no check-in, + the reason for cancellation must be 'no-show' + ------ + Create a cancellation rule that is assigned to a pricelist. Then create + a reservation with a past date and the action_cancel method is launched. + The canceled_reason field is verified to be is equal to "no_show" + """ + Pricelist = self.env["product.pricelist"] + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "pms_property_ids": [self.pms_property1.id], + "penalty_noshow": 50, + } + ) + + self.pricelist = Pricelist.create( + { + "name": "Pricelist Test", + "pms_property_ids": [self.pms_property1.id], + "cancelation_rule_id": self.cancelation_rule.id, + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=-5), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "room_type_id": self.room_type_double.id, + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + reservation.action_cancel() + reservation.flush() + self.assertEqual( + reservation.cancelled_reason, + "noshow", + "If reservation has already passed and no checkin," + "cancelled_reason must be 'noshow'", + ) + + @freeze_time("2012-01-14") + def test_cancelation_reason_intime(self): + """ + Check that if a reservation is canceled on time according + to the cancellation rules the canceled_reason field must be "intime" + ------ + Create a cancellation rule assigned to a price list with + the field days_intime = 3. Then create a reservation with + a checkin date within 5 days and launch the action_cancel method. + canceled_reason field must be "intime" + """ + Pricelist = self.env["product.pricelist"] + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "pms_property_ids": [self.pms_property1.id], + "days_intime": 3, + } + ) + + self.pricelist = Pricelist.create( + { + "name": "Pricelist Test", + "pms_property_ids": [self.pms_property1.id], + "cancelation_rule_id": self.cancelation_rule.id, + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=5), + "checkout": fields.date.today() + datetime.timedelta(days=8), + "room_type_id": self.room_type_double.id, + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + reservation.action_cancel() + reservation.flush() + + self.assertEqual( + reservation.cancelled_reason, "intime", "Cancelled reason must be 'intime'" + ) + + @freeze_time("2012-01-14") + def test_cancelation_reason_late(self): + """ + Check that if a reservation is canceled outside the cancellation + period, the canceled_reason field of the reservation must be "late" . + --------- + Create a cancellation rule with the field days_intime = 3. + A reservation is created with a checkin date for tomorrow and the + action_cancel() method is launched. As the reservation was canceled + after the deadline, the canceled_reason field must be late + """ + Pricelist = self.env["product.pricelist"] + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "pms_property_ids": [self.pms_property1.id], + "days_intime": 3, + } + ) + + self.pricelist = Pricelist.create( + { + "name": "Pricelist Test", + "pms_property_ids": [self.pms_property1.id], + "cancelation_rule_id": self.cancelation_rule.id, + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + self.host1 = self.env["res.partner"].create( + { + "firstname": "Host1", + } + ) + + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=1), + "checkout": fields.date.today() + datetime.timedelta(days=4), + "room_type_id": self.room_type_double.id, + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + reservation.action_cancel() + reservation.flush() + self.assertEqual(reservation.cancelled_reason, "late", "-----------") + + @freeze_time("2012-01-14") + def test_compute_checkin_partner_count(self): + """ + Check that the number of guests of a reservation is equal + to the checkin_partner_count field of that same reservation. + ------------- + Create 2 checkin partners. Create a reservation with those + two checkin partners. The checkin_partner_count field must + be equal to the number of checkin partners in the reservation. + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Miguel", + "mobile": "654667733", + "email": "miguel@example.com", + } + ) + self.host2 = self.env["res.partner"].create( + { + "firstname": "Brais", + "mobile": "654437733", + "email": "brais@example.com", + } + ) + self.reservation = self.env["pms.reservation"].create( + { + "checkin": "2012-01-14", + "checkout": "2012-01-17", + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "adults": 3, + "room_type_id": self.room_type_triple.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.checkin1 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host1.id, + "reservation_id": self.reservation.id, + } + ) + self.checkin2 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host2.id, + "reservation_id": self.reservation.id, + } + ) + + self.reservation.checkin_partner_ids = [ + (6, 0, [self.checkin1.id, self.checkin2.id]) + ] + + self.assertEqual( + self.reservation.checkin_partner_count, + len(self.reservation.checkin_partner_ids), + "Checkin_partner_count must be match with number of checkin_partner_ids", + ) + + @freeze_time("2012-01-14") + def test_compute_checkin_partner_pending_count(self): + """ + Check that the checkin_partner_count field gives + the expected result. + -------------- + Create a reservation with 3 adults and associate 2 + checkin partners with that reservation. The + checkin_partner_pending_count field must be the + same as the difference between the adults in the + reservation and the number of checkin_partner_ids in + the reservation + """ + self.host1 = self.env["res.partner"].create( + { + "firstname": "Miguel", + "mobile": "654667733", + "email": "miguel@example.com", + } + ) + self.host2 = self.env["res.partner"].create( + { + "firstname": "Brais", + "mobile": "654437733", + "email": "brais@example.com", + } + ) + self.reservation = self.env["pms.reservation"].create( + { + "checkin": "2012-01-14", + "checkout": "2012-01-17", + "partner_id": self.host1.id, + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type_triple.id, + "adults": 3, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.checkin1 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host1.id, + "reservation_id": self.reservation.id, + } + ) + self.checkin2 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host2.id, + "reservation_id": self.reservation.id, + } + ) + + self.reservation.checkin_partner_ids = [ + (6, 0, [self.checkin1.id, self.checkin2.id]) + ] + + count_expected = self.reservation.adults - len( + self.reservation.checkin_partner_ids + ) + self.assertEqual( + self.reservation.checkin_partner_pending_count, + count_expected, + "Checkin_partner_pending_count isn't correct", + ) + + @freeze_time("2012-01-14") + def test_reservation_action_checkout_fail(self): + """ + Check that a reservation cannot be checkout because + the checkout is not allowed. + --------------- + Create a reservation and try to launch the action_reservation_checkout + method, but this should throw an error, because for the + checkout to be allowed, the reservation must be in "onboard" + or "departure_delayed" state + """ + host = self.env["res.partner"].create( + { + "firstname": "Miguel", + "mobile": "654667733", + "email": "miguel@example.com", + } + ) + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "partner_id": host.id, + "allowed_checkout": True, + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type_double.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + with self.assertRaises(UserError): + reservation.action_reservation_checkout() + + @freeze_time("2012-01-14") + def test_partner_name_folio(self): + """ + Check that a reservation without a partner_name + is associated with the partner_name of its folio + ---------- + Create a folio with a partner_name. Then create a + reservation with folio_id = folio.id and without + partner_name. The partner name of the reservation + and the folio must be the same + """ + + # ARRANGE + self.folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": "Solón", + } + ) + + self.reservation = self.env["pms.reservation"].create( + { + "checkin": "2012-01-14", + "checkout": "2012-01-17", + "pms_property_id": self.pms_property1.id, + "folio_id": self.folio1.id, + "room_type_id": self.room_type_double.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ACT AND ASSERT + self.assertEqual( + self.folio1.partner_name, + self.reservation.partner_name, + "The folio partner name and the reservation partner name doesn't correspond", + ) + + @freeze_time("2012-01-14") + def test_partner_is_agency_not_invoice_to_agency(self): + """ + Check that a reservation without partner_name but with + an agency whose field invoice_to_agency = False will + be set as partner_name "Reservation_from (agency name)" + ------------- + Create an agency with invoice_to_agency = False + and then create a reservation to which that agency + assigns but does not associate any partner. + Then check that the partner_name of that reservation is "Reservation from (agency name)" + """ + sale_channel1 = self.env["pms.sale.channel"].create( + {"name": "Test Indirect", "channel_type": "indirect"} + ) + agency = self.env["res.partner"].create( + { + "firstname": "partner1", + "is_agency": True, + "sale_channel_id": sale_channel1.id, + } + ) + + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": fields.date.today() + datetime.timedelta(days=150), + "checkout": fields.date.today() + datetime.timedelta(days=152), + "agency_id": agency.id, + "room_type_id": self.room_type_double.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + reservation.flush() + + self.assertEqual( + reservation.partner_name, + "Reservation from " + agency.name, + "Partner name doesn't match with to the expected", + ) + + @freeze_time("2010-11-10") + def test_cancel_discount_board_service(self): + """ + When a reservation is cancelled, service discount in case of board_services + must be equal to the discounts of each reservation_line. + + """ + + # ARRANGE + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "penalty_noshow": 50, + "apply_on_noshow": "all", + } + ) + + self.pricelist1.cancelation_rule_id = self.cancelation_rule.id + + self.product = self.env["product.product"].create( + { + "name": "Product test", + "per_day": True, + "consumed_on": "after", + } + ) + self.board_service = self.env["pms.service"].create( + { + "is_board_service": True, + "product_id": self.product.id, + } + ) + + self.room_type_double.list_price = 25 + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=-3), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "service_ids": [self.board_service.id], + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ACTION + reservation.action_cancel() + reservation.flush() + # ASSERT + self.assertEqual( + reservation.reservation_line_ids.mapped("cancel_discount")[0], + reservation.service_ids.service_line_ids.mapped("cancel_discount")[0], + "Cancel discount of reservation service lines must be the same " + "that reservation board services", + ) + + @freeze_time("2011-10-10") + def _test_cancel_discount_reservation_line(self): + """ + When a reservation is cancelled, cancellation discount is given + by the cancellation rule associated with the reservation pricelist. + Each reservation_line calculates depending on the cancellation + reason which is the correspondig discount. In this case the + cancellation reason is'noshow' and the rule specifies that 50% must + be reducted every day, that is, on each of reseravtion_lines + """ + # ARRANGE + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "penalty_noshow": 50, + "apply_on_noshow": "all", + } + ) + + self.pricelist1.cancelation_rule_id = self.cancelation_rule.id + + self.room_type_double.list_price = 50 + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=-3), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + # ACTION + reservation.action_cancel() + reservation.flush() + + # ASSERT + self.assertEqual( + set(reservation.reservation_line_ids.mapped("cancel_discount")), + {self.cancelation_rule.penalty_noshow}, + "Cancel discount of reservation_lines must be equal than cancellation rule penalty", + ) + + @freeze_time("2011-11-11") + def test_cancel_discount_service(self): + """ + When a reservation is cancelled, service discount in + services that are not board_services ALWAYS have to be 100%, + refardless of the cancellation rule associated with the pricelist + """ + # ARRANGE + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "penalty_noshow": 50, + "apply_on_noshow": "all", + } + ) + + self.pricelist1.cancelation_rule_id = self.cancelation_rule.id + + self.product = self.env["product.product"].create( + { + "name": "Product test", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.service = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": self.product.id, + } + ) + + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=-3), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "service_ids": [self.service.id], + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + expected_cancel_discount = 100 + + # ACTION + reservation.action_cancel() + reservation.flush() + # ASSERT + self.assertEqual( + expected_cancel_discount, + reservation.service_ids.service_line_ids.mapped("cancel_discount")[0], + "Cancel discount of services must be 100%", + ) + + @freeze_time("2011-06-06") + def test_discount_in_service(self): + """ + Discount in pms.service is calculated from the + discounts that each if its service lines has, + in this case when reservation is cancelled a + 50% cancellation discount is applied and + there aren't other different discounts + """ + + # ARRANGE + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "penalty_noshow": 50, + "apply_on_noshow": "all", + } + ) + + self.pricelist1.cancelation_rule_id = self.cancelation_rule.id + + self.product = self.env["product.product"].create( + { + "name": "Product test", + "per_day": True, + "consumed_on": "after", + } + ) + self.board_service = self.env["pms.service"].create( + { + "is_board_service": True, + "product_id": self.product.id, + } + ) + + self.room_type_double.list_price = 25 + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today() + datetime.timedelta(days=-3), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "service_ids": [self.board_service.id], + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + # ACTION + reservation.action_cancel() + reservation.flush() + + expected_discount = sum( + sl.price_day_total * sl.cancel_discount / 100 + for sl in self.board_service.service_line_ids + ) + # ASSERT + self.assertEqual( + expected_discount, + self.board_service.discount, + "Service discount must be the sum of its services_lines discount", + ) + + @freeze_time("2011-11-11") + def test_services_discount_in_reservation(self): + """ + Services discount in reservation is equal to the sum of the discounts of all + its services, whether they are board_services or not + """ + # ARRANGE + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "penalty_noshow": 50, + "apply_on_noshow": "all", + } + ) + + self.pricelist1.cancelation_rule_id = self.cancelation_rule.id + + self.product1 = self.env["product.product"].create( + { + "name": "Product test1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.service = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": self.product1.id, + } + ) + self.service.flush() + self.product2 = self.env["product.product"].create( + { + "name": "Product test 2", + "per_person": True, + "consumed_on": "after", + } + ) + self.board_service = self.env["pms.service"].create( + { + "is_board_service": True, + "product_id": self.product2.id, + } + ) + + self.room_type_double.list_price = 25 + checkin = fields.date.today() + datetime.timedelta(days=-3) + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "service_ids": [self.service.id, self.board_service.id], + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + # ACTION + reservation.action_cancel() + reservation.flush() + + expected_discount = sum(s.discount for s in reservation.service_ids) + + # ASSERT + self.assertEqual( + expected_discount, + reservation.services_discount, + "Services discount isn't the expected", + ) + + @freeze_time("2011-12-12") + def _test_price_services_in_reservation(self): + """ + Service price total in a reservation corresponds to the sum of prices + of all its services less the total discount of that services + """ + # ARRANGE + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "penalty_noshow": 50, + "apply_on_noshow": "all", + } + ) + + self.pricelist1.cancelation_rule_id = self.cancelation_rule.id + + self.product1 = self.env["product.product"].create( + { + "name": "Product test1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.service = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": self.product1.id, + } + ) + self.service.flush() + self.product2 = self.env["product.product"].create( + { + "name": "Product test 2", + "per_person": True, + "consumed_on": "after", + } + ) + self.board_service = self.env["pms.service"].create( + { + "is_board_service": True, + "product_id": self.product2.id, + } + ) + + self.room_type_double.list_price = 25 + checkin = fields.date.today() + datetime.timedelta(days=-3) + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "service_ids": [self.service.id, self.board_service.id], + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + # ACTION + reservation.action_cancel() + reservation.flush() + expected_price = round( + ( + self.service.price_total + + self.board_service.price_total * reservation.adults + ) + - reservation.services_discount, + 2, + ) + # ASSERT + self.assertEqual( + expected_price, + reservation.price_services, + "Services price isn't the expected", + ) + + @freeze_time("2011-08-08") + def test_room_discount_in_reservation(self): + """ + Discount in pms.reservation is calculated from the + discounts that each if its reservation lines has, + in this case when reservation is cancelled a 50% + cancellation discount is applied and + there aren't other different discounts + """ + # ARRANGE + self.cancelation_rule = self.env["pms.cancelation.rule"].create( + { + "name": "Cancelation Rule Test", + "penalty_noshow": 50, + "apply_on_noshow": "all", + } + ) + + self.pricelist1.cancelation_rule_id = self.cancelation_rule.id + + self.room_type_double.list_price = 30 + checkin = fields.date.today() + datetime.timedelta(days=-3) + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + # ACTION + reservation.action_cancel() + reservation.flush() + + expected_discount = sum( + rl.price * rl.cancel_discount / 100 + for rl in reservation.reservation_line_ids + ) + + # ASSERT + self.assertEqual( + expected_discount, + reservation.discount, + "Room discount isn't the expected", + ) + + @freeze_time("2012-01-14") + def test_default_normal_reservation_type(self): + """ + Check that the default reservation type is "normal". + ----------- + A reservation is created without defining the reservation_type + field and it is checked that it is 'normal' + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + self.partner1 = self.env["res.partner"].create({"firstname": "Ana"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner1.id, + } + ) + # ACT + self.room_type_double.write({"list_price": 30}) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "folio_id": folio1.id, + } + ) + # ASSERT + self.assertEqual( + reservation.reservation_type, + "normal", + "The default reservation type should be 'normal'", + ) + + @freeze_time("2012-01-14") + def test_price_normal_reservation(self): + """ + Check the price of a normal type reservation. + ----------- + A reservation is created for a room with price 30. + Then it is verified that the total price of the + reservation is equal to the price of the room multiplied + by the number of days of the reservation. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + # ACT + self.room_type_double.write({"list_price": 30}) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + diff_days = (checkout - checkin).days + expected_price = self.room_type_double.list_price * diff_days + # ASSERT + self.assertEqual( + reservation.price_total, + expected_price, + "The expected price of the reservation is not correct", + ) + + @freeze_time("2012-01-14") + def test_price_staff_reservation(self): + """ + Check that the price of a staff type reservation + is not calculated. + ------------- + A reservation is created with the reservation_type field as 'staff'. + Then it is verified that the price of the reservation is equal to 0. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + # ACT + self.room_type_double.write({"list_price": 30}) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "reservation_type": "staff", + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ASSERT + self.assertEqual( + reservation.price_total, + 0.0, + "The expected price of the reservation is not correct", + ) + + @freeze_time("2012-01-14") + def test_price_out_of_service_reservation(self): + """ + Check that the price of a out type reservation + is not calculated. + ------------- + A reservation is created with the reservation_type field as 'out'. + Then it is verified that the price of the reservation is equal to 0. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + closure_reason = self.env["room.closure.reason"].create( + { + "name": "test closure reason", + "description": "test clopsure reason description", + } + ) + # ACT + self.room_type_double.write({"list_price": 30}) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "reservation_type": "out", + "closure_reason_id": closure_reason.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ASSERT + self.assertEqual( + reservation.price_total, + 0.0, + "The expected price of the reservation is not correct", + ) + + # @freeze_time("2012-01-14") + # def test_no_pricelist_staff_reservation(self): + # """ + # Check that in a staff type reservation the pricelist is False. + # ------------- + # A reservation is created with the reservation_type field as 'staff'. + # Then it is verified that the pricelist of the reservation is False. + # """ + # # ARRANGE + # checkin = fields.date.today() + # checkout = fields.date.today() + datetime.timedelta(days=3) + # # ACT + # reservation = self.env["pms.reservation"].create( + # { + # "checkin": checkin, + # "checkout": checkout, + # "room_type_id": self.room_type_double.id, + # "partner_id": self.partner1.id, + # "pms_property_id": self.pms_property1.id, + # "reservation_type": "staff", + # "sale_channel_origin_id": self.sale_channel_direct.id, + # } + # ) + # + # self.assertFalse( + # reservation.pricelist_id, + # "The pricelist of a staff reservation should be False", + # ) + + @freeze_time("2012-01-14") + def test_no_pricelist_out_reservation(self): + """ + Check that in a out type reservation the pricelist is False. + ------------- + A reservation is created with the reservation_type field as 'out'. + Then it is verified that the pricelist of the reservation is False. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + closure_reason = self.env["room.closure.reason"].create( + { + "name": "test closure reason", + "description": "test clopsure reason description", + } + ) + # ACT + self.room_type_double.write({"list_price": 30}) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "reservation_type": "out", + "closure_reason_id": closure_reason.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + self.assertFalse( + reservation.pricelist_id, + "The pricelist of a out of service reservation should be False", + ) + + @freeze_time("2012-01-14") + def test_reservation_type_by_folio(self): + """ + Check that the reservation type field in a reservation is the + same as the reservation type on the folio that contains + that reservation. + -------------------------- + A folio is created with the field reservation_type as 'staff'. + A reservation is created to which the + value of the folio_id is the id of the previously created + folio. Then it is verified that the value of the reservation_type + field of the reservation is the same that reservation_type in the folio: + 'staff'. + """ + # ARRANGE AND ACT + self.partner1 = self.env["res.partner"].create({"firstname": "Ana"}) + folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner1.id, + "reservation_type": "staff", + } + ) + + reservation1 = self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "folio_id": folio1.id, + } + ) + + # ASSERT + self.assertEqual( + reservation1.reservation_type, + "staff", + "The reservation type of the folio should be 'staff'", + ) + + @freeze_time("2012-01-14") + def test_no_partner_id_out_reservation(self): + """ + Check that a reservation of type out of service does not + have a partner_id. + ------------------ + A reservation is created without a partner_id and with the + value of the field reservation_type as '' out. Then it is + checked that the partner_id field of the reservation is False + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + closure_reason = self.env["room.closure.reason"].create( + { + "name": "test closure reason", + "description": "test clopsure reason description", + } + ) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "pms_property_id": self.pms_property1.id, + "reservation_type": "out", + "closure_reason_id": closure_reason.id, + "partner_name": "Install furniture", + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + self.assertFalse( + reservation.partner_id, + "The partner of an out of service reservation should be False", + ) + + @freeze_time("2012-01-14") + def _test_create_partner_in_reservation(self): + """ + Check that a res_partner is created from a reservation. + ------------ + A reservation is created + and a res.partner + should also be created, which is what is checked after creating + the reservation. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + self.id_category = self.env["res.partner.id_category"].create( + {"name": "DNI", "code": "D"} + ) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "pms_property_id": self.pms_property1.id, + "partner_name": "Elis", + "email": "elis@mail.com", + "mobile": "61568547", + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ASSERT + self.assertTrue(reservation.partner_id, "The partner has not been created") + + @freeze_time("2012-01-14") + def test_auto_complete_partner_mobile(self): + """ + It is checked that the mobile field of the reservation + is correctly added to + a res.partner that exists in + the DB are put in the reservation. + -------------------- + A res.partner is created with the name, mobile and email fields. + Then it is verified that the mobile of the res.partner and that of + the reservation are the same. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "firstname": "Enrique", + "mobile": "654667733", + "email": "enrique@example.com", + } + ) + self.id_category = self.env["res.partner.id_category"].create( + {"name": "DNI", "code": "D"} + ) + self.document_id = self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "61645604S", + "partner_id": partner.id, + } + ) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "pms_property_id": self.pms_property1.id, + "partner_id": partner.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ASSERT + self.assertEqual( + reservation.mobile, + partner.mobile, + "The partner mobile has not autocomplete in reservation", + ) + + @freeze_time("2012-01-14") + def test_auto_complete_partner_email(self): + """ + It is checked that the email field of the reservation + is correctly added to + a res.partner that exists in + the DB are put in the reservation. + -------------------- + A res.partner is created with the name, mobile and email fields. + The document_id is added to the res.partner. + Then it is verified that the email of the res.partner and that of + the reservation are the same. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "firstname": "Simon", + "mobile": "654667733", + "email": "simon@example.com", + } + ) + self.id_category = self.env["res.partner.id_category"].create( + {"name": "DNI", "code": "D"} + ) + self.document_id = self.env["res.partner.id_number"].create( + { + "category_id": self.id_category.id, + "name": "74247377L", + "partner_id": partner.id, + } + ) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "pms_property_id": self.pms_property1.id, + "partner_id": partner.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ASSERT + self.assertEqual( + reservation.email, + partner.email, + "The partner email has not autocomplete in reservation", + ) + + @freeze_time("2012-01-14") + def test_is_possible_customer_by_email(self): + """ + It is checked that the field possible_existing_customer_ids + exists in a reservation with an email from a res.partner saved + in the DB. + ---------------- + A res.partner is created with the name and email fields. A reservation + is created by adding the same email as the res.partner. Then it is + checked that some possible_existing_customer_ids exists. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "firstname": "Courtney Campbell", + "email": "courtney@example.com", + } + ) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "pms_property_id": self.pms_property1.id, + "partner_name": partner.name, + "email": partner.email, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ASSERT + self.assertTrue( + reservation.possible_existing_customer_ids, + "No customer found with this email", + ) + + @freeze_time("2012-01-14") + def test_is_possible_customer_by_mobile(self): + """ + It is checked that the field possible_existing_customer_ids + exists in a reservation with a mobile from a res.partner saved + in the DB. + ---------------- + A res.partner is created with the name and email fields. A reservation + is created by adding the same mobile as the res.partner. Then it is + checked that some possible_existing_customer_ids exists. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "firstname": "Ledicia Sandoval", + "mobile": "615369231", + } + ) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "pms_property_id": self.pms_property1.id, + "partner_name": partner.name, + "mobile": partner.mobile, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ASSERT + self.assertTrue( + reservation.possible_existing_customer_ids, + "No customer found with this mobile", + ) + + @freeze_time("2012-01-14") + def test_add_possible_customer(self): + """ + Check that a partner was correctly added to the reservation + after launching the add_partner() method of the several partners wizard + --------------- + A res.partner is created with name, email and mobile. A reservation is + created with the email field equal to that of the res.partner created before. + The wizard is created with the reservation id and the partner added to the + possible_existing_customer_ids field. The add_partner method of the wizard + is launched and it is checked that the partner was correctly added to the + reservation. + """ + # ARRANGE + partner = self.env["res.partner"].create( + { + "firstname": "Serafín Rivas", + "email": "serafin@example.com", + "mobile": "60595595", + } + ) + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "pms_property_id": self.pms_property1.id, + "partner_name": partner.name, + "email": partner.email, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + several_partners_wizard = self.env["pms.several.partners.wizard"].create( + { + "reservation_id": reservation.id, + "possible_existing_customer_ids": [(6, 0, [partner.id])], + } + ) + + several_partners_wizard.add_partner() + # ASSERT + self.assertEqual( + reservation.partner_id.id, + partner.id, + "The partner was not added to the reservation ", + ) + + @freeze_time("2012-01-14") + def test_not_add_several_possibles_customers(self): + """ + Check that multiple partners cannot be added to a reservation + from the several partners wizard. + --------------- + Two res.partner are created with name, email and mobile. A reservation is + created with the email field equal to that of the partner1 created before. + The wizard is created with the reservation id and the two partners added to the + possible_existing_customer_ids field. The add_partner method of the wizard + is launched and it is verified that a Validation_Error was raised. + """ + # ARRANGE + partner1 = self.env["res.partner"].create( + { + "firstname": "Serafín Rivas", + "email": "serafin@example.com", + "mobile": "60595595", + } + ) + partner2 = self.env["res.partner"].create( + { + "firstname": "Simon", + "mobile": "654667733", + "email": "simon@example.com", + } + ) + + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "pms_property_id": self.pms_property1.id, + "partner_name": partner1.name, + "email": partner1.email, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + several_partners_wizard = self.env["pms.several.partners.wizard"].create( + { + "reservation_id": reservation.id, + "possible_existing_customer_ids": [(6, 0, [partner1.id, partner2.id])], + } + ) + + # ACT AND ASSERT + with self.assertRaises( + ValidationError, + msg="Two partners cannot be added to the reservation", + ): + several_partners_wizard.add_partner() + + @freeze_time("2012-01-14") + def test_not_add_any_possibles_customers(self): + """ + Check that the possible_existing_customer_ids field of the several + partners wizard can be left empty and then launch the add_partner() + method of this wizard to add a partner in reservation. + --------------- + A reservation is created. The wizard is created without the + possible_existing_customer_ids field. The add_partner method of + the wizard is launched and it is verified that a Validation_Error + was raised. + """ + + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=3) + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "pms_property_id": self.pms_property1.id, + "partner_name": "Rosa Costa", + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + several_partners_wizard = self.env["pms.several.partners.wizard"].create( + { + "reservation_id": reservation.id, + } + ) + + # ACT AND ASSERT + with self.assertRaises( + ValidationError, + msg="A partner must be added to the reservation", + ): + several_partners_wizard.add_partner() + + @freeze_time("1991-11-10") + def test_commission_amount_with_board_service(self): + """ + Check if commission in reservation is correctly calculated + when reservation has services and board_services + + Create a service that isn't board service. + Create a service that is board service. + Create a reservation with that service and board_service. + + In this case when the reservation is made through an agency + that has a default commission, this commission is applied to the + price of the room and the price of services that correspond with + board service. + + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Product test1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.service = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": self.product1.id, + "pms_property_id": self.pms_property1.id, + } + ) + self.service.flush() + self.product_test1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + } + ) + self.board_service_test = self.env["pms.board.service"].create( + { + "name": "Test Board Service", + "default_code": "TPS", + "pms_property_ids": [self.pms_property1.id], + } + ) + + self.env["pms.board.service.line"].create( + { + "pms_board_service_id": self.board_service_test.id, + "product_id": self.product_test1.id, + "amount": 8, + "adults": True, + } + ) + self.board_service_room_type = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type_double.id, + "pms_board_service_id": self.board_service_test.id, + "pms_property_id": self.pms_property1.id, + } + ) + checkin = fields.date.today() + checkout = checkin + datetime.timedelta(days=11) + reservation_vals = { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "agency_id": self.agency1.id, + "service_ids": [self.service.id], + "sale_channel_origin_id": self.sale_channel_direct.id, + } + # ACT + reservation = self.env["pms.reservation"].create(reservation_vals) + reservation.write( + { + "board_service_room_id": self.board_service_room_type.id, + } + ) + + self.commission = ( + reservation.price_total * self.agency1.default_commission / 100 + ) + for service in reservation.service_ids: + if service.is_board_service: + self.commission = ( + self.commission + + service.price_total * self.agency1.default_commission / 100 + ) + # ASSERT + self.assertEqual( + self.commission, + reservation.commission_amount, + "Reservation commission is wrong", + ) + + @freeze_time("2012-01-14") + def test_closure_reason_out_of_service_mandatory_not(self): + """ + Ouf of service reservation should contain a closure reason id. + ------------- + Create a reservation of type out of service and check if there's no + closure reason id should raises an exception. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="The reservation has been created and it shouldn't, " + "because it doesn't have a closure reason.", + ): + self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "reservation_type": "out", + } + ) + + @freeze_time("2012-01-14") + def test_closure_reason_out_of_service_mandatory(self): + """ + Ouf of service reservation should contain a closure reason id. + ------------- + Create a reservation of type out of service and with a closure reason. + """ + # ARRANGE + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + closure_reason = self.env["room.closure.reason"].create( + { + "name": "Room revision", + "description": "Revision of lights, " + "fire extinguishers, smoke detectors and " + "emergency lights", + } + ) + # ACT + reservation_out = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "reservation_type": "out", + "closure_reason_id": closure_reason, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + # ASSERT + self.assertTrue( + reservation_out.closure_reason_id, + "The out of service reservation should be created properly with " + "a closure reason.", + ) + + # tests for several sale channels in reservation + @freeze_time("2000-11-10") + def test_reservation_sale_channel_origin_in_reservation_lines(self): + """ + Check that reservation_lines have sale_channel_id + corresponding to sale_channel_origin_id of their reservation + + When a reservation was created with a sale channel, it corresponds + to the sale_channel_origin. + Reservation lines will have as sale_channel_id the sale_channel_origin_id + of reservation when creating them + + """ + # ARRANGE + checkin = fields.date.today() + checkout = checkin + datetime.timedelta(days=3) + reservation_vals = { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + + # ACT + reservation = self.env["pms.reservation"].create(reservation_vals) + + # ASSERT + self.assertEqual( + reservation.sale_channel_origin_id, + reservation.reservation_line_ids.mapped("sale_channel_id"), + "Sale channel of reservation lines must be the same that their reservation", + ) + + @freeze_time("2000-10-10") + def test_reservation_sale_channel_origin_in_folio(self): + """ + Check that folio have sale_channel_origin_id + corresponding to sale_channel_origin_id of the reservation + that was created before the folio + + When a reservation was created with a sale channel, it corresponds + to the sale_channel_origin. + If reservation didn't have folio previously, the folio to be created + will have the same sale_channel_origin as the reservation + + """ + # ARRANGE + checkin = fields.date.today() + checkout = checkin + datetime.timedelta(days=3) + reservation_vals = { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + + # ACT + reservation = self.env["pms.reservation"].create(reservation_vals) + + # ASSERT + self.assertEqual( + reservation.sale_channel_origin_id, + reservation.folio_id.sale_channel_origin_id, + "Sale channel of folio must be the same that it reservation", + ) + + @freeze_time("2001-10-15") + def test_reservation_sale_channel_origin_of_folio(self): + """ + Check that the reservation has sale_channel_origin_id + as the folio sale_channel_origin_id in + which reservation was created when a folio has already + another reservations. + + Testing whether it works when the folio sale_channel_origin_id + is given by a previously created reservation + + When a reservation is created on a folio + that already has a sale_channel_origin + that reservation will have the same sale_channel_origin + + """ + # ARRANGE + reservation_vals = { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + reservation1 = self.env["pms.reservation"].create(reservation_vals) + # ACT + reservation2 = self.env["pms.reservation"].create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "folio_id": reservation1.folio_id.id, + } + ) + # ASSERT + self.assertEqual( + reservation1.sale_channel_origin_id.id, + reservation2.sale_channel_origin_id.id, + "Sale channel of reservations must be the same", + ) + + @freeze_time("2000-10-19") + def test_reservation_lines_same_sale_channel(self): + """ + Check if sale_channel_ids of reservation correspond to + sale_channel_id of its reservation. + + In this case, the reservation has several reservation_lines + with the same sale_channel_id. Reservation lines are created + with sale_channel_origin_id of the reservation and haven't been + modified. + + """ + # ARRANGE + reservation_vals = { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=4), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + # ACT + reservation1 = self.env["pms.reservation"].create(reservation_vals) + + # ASSERT + self.assertEqual( + reservation1.sale_channel_ids, + reservation1.reservation_line_ids.mapped("sale_channel_id"), + "Sale_channel_ids of reservation must be the same as " + "sale channels of its reservation lines", + ) + + @freeze_time("2000-10-24") + def test_reservation_sale_channel_origin_change_check_lines(self): + """ + Check that sale_channel_id of reservation_lines changes when + sale_channel_origin_id of its reservation has changed + """ + # ARRANGE + sale_channel_direct2 = self.env["pms.sale.channel"].create( + { + "name": "sale channel 2", + "channel_type": "direct", + } + ) + reservation_vals = { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=4), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + reservation1 = self.env["pms.reservation"].create(reservation_vals) + + # ACT + reservation1.sale_channel_origin_id = sale_channel_direct2.id + + # ASSERT + self.assertEqual( + reservation1.sale_channel_origin_id, + reservation1.reservation_line_ids.mapped("sale_channel_id"), + "Sale_channel_id of reservation lines must also be changed", + ) + + @freeze_time("2000-10-29") + def test_reservation_lines_not_change_sale_channel(self): + """ + Check that when changing sale_channel_origin_id of a reservation, + reservation lines that didn't have the same sale_channel_id didn't + change it + + Reservation1: + --> sale_channel_origin_id : sale_channel1.id + --> reservation_lines: + --> 1: sale_channel_id: sale_channel1.id + --> 2: sale_channel_id: sale_channel1.id + --> 3: sale_channel_id: sale_channel1.id + --> 4: sale_channel_id: sale_channel_phone.id + + Change reservation1.sale_channel_origin_id = sale_channel_mail.id + + Expected result: + Reservation1: + --> sale_channel_origin_id : sale_channel_mail.id + --> reservation_lines: + --> 1: sale_channel_id: sale_channel_mail.id + --> 2: sale_channel_id: sale_channel_mail.id + --> 3: sale_channel_id: sale_channel_mail.id + --> 4: sale_channel_id: sale_channel_phone.id + + In short, sale channel of those reservations lines of the reservation + that didn't coincide with sale chanel origin that has been modified, + shouldn't be changed. That is, the last reservation_line must have + sale_channel_id = sale_channel_phone + """ + # ARRANGE + sale_channel_phone = self.env["pms.sale.channel"].create( + { + "name": "phone", + "channel_type": "direct", + } + ) + sale_channel_mail = self.env["pms.sale.channel"].create( + { + "name": "mail", + "channel_type": "direct", + } + ) + reservation_vals = { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=4), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + reservation1 = self.env["pms.reservation"].create(reservation_vals) + + # ACT + reservation_lines = reservation1.reservation_line_ids + reservation_lines[ + len(reservation_lines) - 1 + ].sale_channel_id = sale_channel_phone.id + + reservation1.sale_channel_origin_id = sale_channel_mail + + # ASSERT + self.assertNotEqual( + reservation1.sale_channel_origin_id, + reservation_lines[len(reservation_lines) - 1].sale_channel_id, + "Sale_channel_id of that reservation line shouldn't have changed", + ) + + @freeze_time("2000-11-29") + def test_several_sale_channel_in_lines(self): + """ + Check that when a reservation has more than one sale_channel_id + in its reservation_lines, sale_channel_ids of reservation is well + calculated + """ + # ARRANGE + sale_channel_phone = self.env["pms.sale.channel"].create( + { + "name": "phone", + "channel_type": "direct", + } + ) + reservation_vals = { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=4), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + reservation1 = self.env["pms.reservation"].create(reservation_vals) + + # ACT + reservation_lines = reservation1.reservation_line_ids + reservation_lines[0].sale_channel_id = sale_channel_phone.id + + expected_sale_channels = [] + for line in reservation_lines: + expected_sale_channels.append(line.sale_channel_id.id) + + # ASSERT + self.assertItemsEqual( + reservation1.sale_channel_ids.ids, + list(set(expected_sale_channels)), + "Sale_channel_ids of that reservation must match those of its lines", + ) + + @freeze_time("2000-12-01") + def test_reservation_no_sale_channel_origin(self): + """ + Check that you can't create a reservation without sale_channel_origin + """ + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Error, it has been allowed to create a reservation without sale channel", + ): + self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=4), + } + ) + + @freeze_time("2000-12-01") + def test_one_reservation_change_sale_channel_origin(self): + """ + Check that when changing the sale_channel_origin of a reservation, + sale_channel_origin of its folio changes if folio only has one reservation + """ + # ARRANGE + sale_channel_phone = self.env["pms.sale.channel"].create( + { + "name": "phone", + "channel_type": "direct", + } + ) + reservation_vals = { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=4), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + reservation1 = self.env["pms.reservation"].create(reservation_vals) + + # ACT + reservation1.sale_channel_origin_id = sale_channel_phone.id + + # ASSERT + self.assertEqual( + reservation1.folio_id.sale_channel_origin_id, + reservation1.sale_channel_origin_id, + "Sale_channel_origin_id of folio must be the same as " + "sale_channel_origin of rservation", + ) + + # TEMPORAL UNABLE (_check_lines_with_sale_channel_id in pms_reservation.py + # unable to allow updagrade version) + # @freeze_time("2000-12-10") + # def test_check_sale_channel_origin_in_reservation_lines(self): + # """ + # Check that a reservation has at least one reservation_line woth the + # same sale_channel_id as its sale_channel_origin_id + # """ + # # ARRANGE + # sale_channel_phone = self.env["pms.sale.channel"].create( + # { + # "name": "phone", + # "channel_type": "direct", + # } + # ) + # reservation_vals = { + # "checkin": datetime.datetime.now(), + # "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + # "room_type_id": self.room_type_double.id, + # "partner_id": self.partner1.id, + # "pms_property_id": self.pms_property1.id, + # "sale_channel_origin_id": self.sale_channel_direct.id, + # } + # reservation1 = self.env["pms.reservation"].create(reservation_vals) + # reservation1.fetch() + # # ACT & ASSERT + # with self.assertRaises( + # ValidationError, + # msg=""" + # Error, there cannot be a reservation + # in which at least one of its reservation + # """ + # "lines doesn't have as sale_channel_id the sale_channel_origin_id of reservation", + # ): + # reservation1.reservation_line_ids.write( + # {"sale_channel_id": sale_channel_phone} + # ) + # reservation1.flush() diff --git a/pms/tests/test_pms_room.py b/pms/tests/test_pms_room.py new file mode 100644 index 0000000000..2854239797 --- /dev/null +++ b/pms/tests/test_pms_room.py @@ -0,0 +1,432 @@ +from psycopg2 import IntegrityError + +from odoo.exceptions import ValidationError +from odoo.tools import mute_logger + +from .common import TestPms + + +class TestPmsRoom(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.pms_property2 = cls.env["pms.property"].create( + { + "name": "Property_2", + "company_id": cls.company1.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + + cls.room_type1 = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id, cls.pms_property2.id], + "name": "Single", + "default_code": "SIN", + "class_id": cls.room_type_class1.id, + "list_price": 30, + } + ) + + @mute_logger("odoo.sql_db") + def test_room_name_uniqueness_by_property(self): + """ + Check that there are no two rooms with the same name in the same property + PRE: - room1 'Room 101' exists + - room1 has pms_property1 + ACT: - create a new room2 + - room2 has name 'Room 101' + - room2 has pms_property1 + POST: - Integrity error: already exists another room + with the same name on the same property + - room2 not created + """ + # ARRANGE + self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + } + ) + # ACT & ASSERT + with self.assertRaises( + IntegrityError, + msg="The room should not be created if its name is equal " + "to another room that belongs to the same property.", + ): + self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + } + ) + + def test_room_name_duplicated_different_property(self): + """ + Check that two rooms with the same name can exist in multiple properties + PRE: - room1 'Room 101' exists + - room1 has pms_property1 + ACT: - create a new room2 + - room2 has name 'Room 101' + - room2 has pms_property2 + POST: - room2 created + """ + # ARRANGE + self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + } + ) + # ACT & ASSERT + try: + self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property2.id, + "room_type_id": self.room_type1.id, + } + ) + except IntegrityError: + self.fail( + "The room should be created even if its name is equal " + "to another room, but that room not belongs to the same property." + ) + + def test_display_name_room(self): + """ + Check that the display_name field of a room is as expected. + ------------ + A room is created and then it is checked that the display name + field of this is composed of: + room.name [room_type.default_code] + """ + self.room1 = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + } + ) + expected_display_name = "%s [%s]" % ( + self.room1.name, + self.room_type1.default_code, + ) + self.assertEqual( + self.room1.display_name, + expected_display_name, + "The display name of the room is not as expected", + ) + + def test_display_name_room_with_amenity(self): + """ + Check that the display_name field of a room with one amenity + is as expected. + ------------ + A amenity is created with default code and with is_add_code_room_name + field as True. A room is created in which the amenity created before + is added in the room_amenity_ids field and then it is verified that + the display name field of this is composed of: + room.name [room_type.default_code] amenity.default_code + """ + self.amenity_type1 = self.env["pms.amenity.type"].create( + { + "name": "Amenity Type 1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + self.amenity1 = self.env["pms.amenity"].create( + { + "name": "Amenity 1", + "pms_amenity_type_id": self.amenity_type1.id, + "default_code": "A1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "is_add_code_room_name": True, + } + ) + self.room1 = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + "room_amenity_ids": [(6, 0, [self.amenity1.id])], + } + ) + expected_display_name = "%s [%s] %s" % ( + self.room1.name, + self.room_type1.default_code, + self.amenity1.default_code, + ) + self.assertEqual( + self.room1.display_name, + expected_display_name, + "The display name of the room is not as expected", + ) + + def test_display_name_room_with_several_amenities(self): + """ + Check that the display_name field of a room with several amenities + is as expected. + ------------ + Two amenities are created with diferent default code and with is_add_code_room_name + field as True. A room is created in which the amenities created before are added in + the room_amenity_ids field and then it is verified that the display name field of this + is composed of: + room.name [room_type.default_code] amenity1.default_code amenity2.default_code + """ + self.amenity_type1 = self.env["pms.amenity.type"].create( + { + "name": "Amenity Type 1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + self.amenity1 = self.env["pms.amenity"].create( + { + "name": "Amenity 1", + "pms_amenity_type_id": self.amenity_type1.id, + "default_code": "A1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "is_add_code_room_name": True, + } + ) + self.amenity2 = self.env["pms.amenity"].create( + { + "name": "Amenity 2", + "pms_amenity_type_id": self.amenity_type1.id, + "default_code": "B1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "is_add_code_room_name": True, + } + ) + self.room1 = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + "room_amenity_ids": [(6, 0, [self.amenity1.id, self.amenity2.id])], + } + ) + expected_display_name = "%s [%s] %s %s" % ( + self.room1.name, + self.room_type1.default_code, + self.amenity1.default_code, + self.amenity2.default_code, + ) + self.assertEqual( + self.room1.display_name, + expected_display_name, + "The display name of the room is not as expected", + ) + + def test_short_name_room_name_gt_4(self): + """ + It checks through subtest that the short names of the + rooms are correctly established when the names of these + exceed 4 characters. + ------------------------------------------------------- + First a room_type (Sweet Imperial) is created. Then 6 rooms + are created with the name Sweet Imperial + room number. Finally + in a loop we check that the short name of the rooms was set + correctly: 'SW01, SW02, SW03...' + """ + self.room_type2 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Sweet Imperial", + "default_code": "SWI", + "class_id": self.room_type_class1.id, + "list_price": 100, + } + ) + rooms = [] + self.room1 = self.env["pms.room"].create( + { + "name": "Sweet Imperial 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type2.id, + } + ) + rooms.append(self.room1) + self.room2 = self.env["pms.room"].create( + { + "name": "Sweet Imperial 102", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type2.id, + } + ) + rooms.append(self.room2) + self.room3 = self.env["pms.room"].create( + { + "name": "Sweet Imperial 103", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type2.id, + } + ) + rooms.append(self.room3) + self.room4 = self.env["pms.room"].create( + { + "name": "Sweet Imperial 104", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type2.id, + } + ) + rooms.append(self.room4) + self.room5 = self.env["pms.room"].create( + { + "name": "Sweet Imperial 105", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type2.id, + } + ) + rooms.append(self.room5) + self.room6 = self.env["pms.room"].create( + { + "name": "Sweet Imperial 106", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type2.id, + } + ) + rooms.append(self.room6) + for index, room in enumerate(rooms, start=1): + with self.subTest(room): + self.assertEqual( + room.short_name, + "SW0" + str(index), + "The short name of the room should be SW0" + str(index), + ) + + def test_short_name_room_name_lt_4(self): + """ + Checks that the short name of a room is equal to the name + when it does not exceed 4 characters. + --------------------------------------------------------- + A room is created with a name less than 4 characters (101). + Then it is verified that the short name and the name of the + room are the same. + """ + self.room1 = self.env["pms.room"].create( + { + "name": "101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + } + ) + self.assertEqual( + self.room1.short_name, + self.room1.name, + "The short name of the room should be equal to the name of the room", + ) + + def test_short_name_gt_4_constraint(self): + """ + Check that the short name of a room cannot exceed 4 characters. + -------------------------------------------------------------- + A room named 201 is created. Afterwards, it is verified that a + ValidationError is thrown when trying to change the short name + of that room to 'SIN-201'. + """ + self.room1 = self.env["pms.room"].create( + { + "name": "201", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + } + ) + + with self.assertRaises( + ValidationError, + msg="The short_name of the room should not be able to be write.", + ): + self.room1.write({"short_name": "SIN-201"}) + + def test_create_independent_address(self): + """ + Check that an independent address is created and associated correctly + when address_is_independent is set to True. + """ + self.room1 = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + "address_is_independent": True, + } + ) + self.assertTrue( + self.room1.address_id, + "The address should be created and associated with the room.", + ) + self.assertTrue(self.room1.address_id.active, "The address should be active.") + + def test_deactivate_independent_address(self): + """ + Check that the independent address is archived when a + ddress_is_independent is set to False. + """ + self.room1 = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + "address_is_independent": True, + } + ) + self.room1.address_is_independent = False + self.assertFalse( + self.room1.address_id.active, "The address should be archived." + ) + + def test_reactivate_independent_address(self): + """ + Check that the archived independent address is reactivated when + address_is_independent is set to True again. + """ + self.room1 = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + "address_is_independent": True, + } + ) + initial_address_id = self.room1.address_id.id + self.room1.address_is_independent = False + self.assertFalse( + self.room1.address_id.active, "The address should be archived." + ) + self.room1.address_is_independent = True + self.assertTrue( + self.room1.address_id.active, + "The address should be reactivated, not created again.", + ) + self.assertEqual( + self.room1.address_id.id, + initial_address_id, + "The reactivated address should be the same as the initial address.", + ) + self.assertEqual( + self.room1.address_id.name, + "Room 101", + "The reactivated address should have the same name.", + ) + + def test_prevent_deletion_of_associated_address(self): + """ + Check that an associated address cannot be deleted. + """ + self.room1 = self.env["pms.room"].create( + { + "name": "Room 101", + "pms_property_id": self.pms_property1.id, + "room_type_id": self.room_type1.id, + "address_is_independent": True, + } + ) + with self.assertRaises( + IntegrityError, + msg="The address associated with a room should not be deletable.", + ): + self.room1.address_id.unlink() diff --git a/pms/tests/test_pms_room_type.py b/pms/tests/test_pms_room_type.py new file mode 100644 index 0000000000..bb25d6a19f --- /dev/null +++ b/pms/tests/test_pms_room_type.py @@ -0,0 +1,886 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import UserError, ValidationError + +from .common import TestPms + + +class TestRoomType(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.pms_property2 = cls.env["pms.property"].create( + { + "name": "Property 2", + "company_id": cls.company1.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + cls.company2 = cls.env["res.company"].create( + { + "name": "Company 2", + } + ) + + cls.pms_property3 = cls.env["pms.property"].create( + { + "name": "Property 3", + "company_id": cls.company2.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + + def test_create_room_type_consistency_company(self): + """ + Create a room type with a company (1) consistent with the company property (1). + Creation should be successful. + + PRE: - room_type1 does not exists + ACT: - create a new room_type1 room + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 has company company1 + - room_type1 has company company1 + POST: - room_type1 created + """ + # ARRANGE & ACT + new_room_type = self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": self.company1.id, + "class_id": self.room_type_class1.id, + } + ) + # ASSERT + self.assertTrue(new_room_type.id, "Room type not created when it should") + + def test_create_room_type_inconsistency_company(self): + """ + Create a room type with inconsistency between company (1) + and company property (1). + The creation should fail. + + PRE: - room_type1 does not exists + ACT: - create a new room_type1 room + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 has company1 + - room_type1 has company2 + POST: - Integrity error, pms_property1 has company1 and room type company2 + - room_type1 not created + """ + # ARRANGE & ACT & ASSERT + with self.assertRaises( + UserError, msg="The room type has been created and it shouldn't" + ): + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": self.company2.id, + "class_id": self.room_type_class1.id, + } + ) + + def test_create_room_type_inconsistency_companies(self): + """ + Create a room type with inconsistency between company (1) + and company properties (several). + The creation should fail. + + PRE: - room_type1 does not exists + ACT: - create a new room_type1 room + - room_type1 has code c1 + - room_type1 has property pms_property1 and pms_property3 + - pms_property1 has company company1 + - pms_property3 has company2 + - room_type1 has company2 + POST: - Integrity error, pms_property1 has company1 and room type company2 + - room_type1 not created + """ + # ARRANGE & ACT & ASSERT + with self.assertRaises( + UserError, msg="The room type has been created and it shouldn't" + ): + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property3.id]) + ], + "company_id": self.company2.id, + "class_id": self.room_type_class1.id, + } + ) + + def test_create_room_type_consistency_companies(self): + """ + Create a room type with consistency between companies (all) + and company properties (2). + Creation should be successful. + + PRE: - room_type1 does not exists + ACT: - create a new room_type1 + - room_type1 has code c1 + - room_type1 has property pms_property1 and pms_property3 + - pms_property1 has company company1 + - pms_property3 has company2 + - room_type1 has no company + POST: - room_type1 created + """ + # ARRANGE & ACT + new_room_type = self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property3.id]) + ], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + # ASSERT + self.assertTrue(new_room_type.id, "Room type not created when it should") + + # external integrity + def test_create_room_type_inconsistency_all_companies(self): + """ + Create a room type for 1 company and 1 property. + Try to create a room type for all the companies. + The creation should fail. + + PRE: - room type room_type1 exists + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 has company company1 + - room_type1 has no company + ACT: - create a new room_type2 room + - room_type2 has code c1 + - room_type2 has property pms_property1 + - pms_property1 has company company1 + - room_type2 has no company + POST: - Integrity error: the room type already exists + - room_type2 not created + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="The room type has been created and it shouldn't" + ): + # room_type2 + self.env["pms.room.type"].create( + { + "name": "Room type 2", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + + def test_create_room_type_inconsistency_code_company(self): + """ + Create a room type for all the companies and for one property. + Try to create a room type with same code and same property but + for all companies. + The creation should fail. + + PRE: - room type room_type1 exists + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 has company company1 + - room_type1 has no company + ACT: - create a new room_type2 room + - room_type2 has code c1 + - room_type2 has property pms_property1 + - pms_property1 has company company1 + - room_type2 has company company1 + POST: - Integrity error: the room type already exists + - room_type2 not created + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="The room type has been created and it shouldn't" + ): + # room_type2 + self.env["pms.room.type"].create( + { + "name": "Room type 2", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": self.company1.id, + "class_id": self.room_type_class1.id, + } + ) + + def test_create_room_type_inconsistency_code_companies(self): + """ + Create a room type for 1 property and 1 company. + Try to create a room type with same code and 3 propertys + belonging to 2 different companies. + The creation should fail. + + PRE: - room type room_type1 exists + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 has company company1 + - room_type1 has company company1 + ACT: - create a new room_type2 room + - room_type2 has code c1 + - room_type2 has property pms_property1, pms_property2, pms_property3 + - pms_property1, pms_property2 has company company1 + - pms_property3 has company2 + - room_type2 has no company + POST: - Integrity error: the room type already exists + - room_type not created + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": self.company1.id, + "class_id": self.room_type_class1.id, + } + ) + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="The room type has been created and it shouldn't" + ): + # room_type2 + self.env["pms.room.type"].create( + { + "name": "Room type 2", + "default_code": "c1", + "pms_property_ids": [ + ( + 6, + 0, + [ + self.pms_property1.id, + self.pms_property2.id, + self.pms_property3.id, + ], + ) + ], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + + def test_get_room_type_by_property_first(self): + """ + Room type exists for all the companies and 2 properties. + Search for property of existing room type. + The method should return the existing room type. + + PRE: - room type room_type1 exists + - room_type1 has code c1 + - room_type1 with 2 properties pms_property1 and pms_property2 + - pms_property1 and pms_property2 have the same company company1 + - room_type1 has no company + ACT: - search room type with code c1 and pms_property1 + - pms_property1 has company company1 + POST: - only room_type1 room type found + """ + # ARRANGE + room_type1 = self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property3.id]) + ], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property1.id, "c1" + ) + # ASSERT + self.assertEqual(room_types.id, room_type1.id, "Expected room type not found") + + def test_get_room_type_by_property_second(self): + """ + Room type exists for all the companies and 2 properties. + Search for 2nd property of existing room type. + The method should return existing room type. + + PRE: - room type room_type1 exists + - room_type1 has code c1 + - room_type1 with 2 properties pms_property1 and pms_property2 + - pms_property1 and pms_property2 have same company company1 + - room_type1 has no company + ACT: - search room type with code c1 and property pms_property3 + - pms_property3 have company2 + POST: - no room type found + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property2.id]) + ], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property3.id, "c1" + ) + + # ASSERT + self.assertFalse(room_types, "Room type found but it should have not found any") + + def test_get_room_type_by_property_existing_all_same_company(self): + """ + Room type exists for 1 company and for all properties. + Search for one specific property belonging to same company + as the existing room type. + The method should return existing room type. + + PRE: - room type r1 exists + - room_type1 has code c1 + - room_type1 properties are null + - room_type1 company is company1 + ACT: - search room type with code c1 and pms_property1 + - pms_property1 have company company1 + POST: - only rroom_type1 room type found + """ + # ARRANGE + room_type1 = self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": False, + "company_id": self.company1.id, + "class_id": self.room_type_class1.id, + } + ) + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property1.id, "c1" + ) + # ASSERT + self.assertEqual(room_types.id, room_type1.id, "Expected room type not found") + + def test_get_room_type_by_property_existing_all_diff_company(self): + """ + Room type exists for 1 company and all the properties. + Search for property different than existing room type. + The method shouldn't return results. + + PRE: - room type room_type1 exists + - room_type1 has code c1 + - room_type1 properties are null + - room_type1 company is company1 + ACT: - search room type with code c1 and pms_property3 + - pms_property3 have company2 + POST: - no room type found + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type r1", + "default_code": "c1", + "pms_property_ids": False, + "company_id": self.company1.id, + "class_id": self.room_type_class1.id, + } + ) + + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property3.id, "c1" + ) + + # ASSERT + self.assertFalse(room_types, "Room type found but it should have not found any") + + # tests with more than one room type + def test_get_room_type_by_property_existing_several_match_prop(self): + """ + Room type 1 exists for all companies and 2 properties. + Room type 2 exists for all companies and properties. + Search for same property as one of the 1st room type created. + The method should return the 1st room type created. + + PRE: - room type room_type1 exists + - room_type1 has code c1 + - room_type1 with 2 properties pms_property1 and pms_property2 + - pms_property1 and pms_property2 have the same company company1 + - room_type1 has no company + - room type room_type2 exists + - room_type2 has code c1 + - room_type2 has no properties + - room_type2 has no company + ACT: - search room type with code c1 and property pms_property1 + - pms_property1 have company company1 + POST: - only room_type1 room type found + """ + # ARRANGE + room_type1 = self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property3.id]) + ], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + # room_type2 + self.env["pms.room.type"].create( + { + "name": "Room type 2", + "default_code": "c1", + "pms_property_ids": False, + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property1.id, "c1" + ) + + # ASSERT + self.assertEqual(room_types.id, room_type1.id, "Expected room type not found") + + def test_get_room_type_by_property_diff_company(self): + """ + Room type 1 exists for all companies and one property. + Room type 2 exists for all companies and properties. + Search for property different than the 1st room type created + and belonging to different company. + The method should return the 2nd room type created. + + PRE: - room type r1 exists + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 have the company company1 + - room_type1 has no company + - room type room_type2 exists + - room_type2 has code c1 + - room_type2 has no properties + - room_type2 has no company + ACT: - search room type with code c1 and property pms_property2 + - pms_property2 have company company1 + POST: - only room_type1 room type found + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + room_type2 = self.env["pms.room.type"].create( + { + "name": "Room type 2", + "default_code": "c1", + "pms_property_ids": False, + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property2.id, "c1" + ) + + # ASSERT + self.assertEqual(room_types.id, room_type2.id, "Expected room type not found") + + def test_get_room_type_by_property_same_company(self): + """ + Room type 1 exists for all companies and one property. + Room type 2 exists for all companies and properties. + Search for property different than the 1st room type created + and belonging to same company. + The method should return the 2nd room type created. + + PRE: - room type room_type1 exists + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 have the company company1 + - room_type1 has no company + - room type room_type2 exists + - room_type2 has code c1 + - room_type2 has no properties + - room_type2 has no company + ACT: - search room type with code c1 and pms_property3 + - pms_property3 have company2 + POST: - only room_type2 room type found + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + room_type2 = self.env["pms.room.type"].create( + { + "name": "Room type 2", + "default_code": "c1", + "pms_property_ids": False, + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property3.id, "c1" + ) + + # ASSERT + self.assertEqual(room_types.id, room_type2.id, "Expected room type not found") + + def test_get_room_type_by_property_same_company_prop_not_found(self): + """ + Room type 1 exists for all companies and one property. + Room type 2 exists for one company and for all properties. + Search for property different than the + 1st room type created but belonging to same company. + The method shouldn't return results. + + PRE: - room_type1 exists + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 have the company company1 + - room_type1 has no company + - room_type2 exists + - room_type2 has code c1 + - room_type2 has no properties + - room_type2 has company company1 + ACT: - search room type with code c1 and pms_property3 + - pms_property3 have company2 + POST: - no room type found + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + # room_type2 + self.env["pms.room.type"].create( + { + "name": "Room type 2", + "default_code": "c1", + "pms_property_ids": False, + "company_id": self.company1.id, + "class_id": self.room_type_class1.id, + } + ) + + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property3.id, "c1" + ) + + # ASSERT + self.assertFalse(room_types, "Room type found but it should have not found any") + + def test_get_room_type_by_property_same_company_prop(self): + """ + Room type 1 exists for all companies and for one property. + Room type 2 exists for one company and for all properties. + Search for property belonging to the same company as + 2nd room type created. + The method should return 2nd existing room type. + + PRE: - room_type1 exists + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 have the company company1 + - room_type1 has no company + - room_type2 exists + - room_type2 has code c1 + - room_type2 has no properties + - room_type2 has company2 + ACT: - search room type with code c1 and pms_property3 + - pms_property3 have company2 + POST: - room_type2 room type found + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + room_type2 = self.env["pms.room.type"].create( + { + "name": "Room type 2", + "default_code": "c1", + "pms_property_ids": False, + "company_id": self.company2.id, + "class_id": self.room_type_class1.id, + } + ) + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property3.id, "c1" + ) + + # ASSERT + self.assertEqual(room_types.id, room_type2.id, "Expected room type not found") + + def test_get_room_type_by_property_diff_company_prop(self): + """ + Room type 1 exists for all companies and for one property. + Room type 2 exists for one company and for all properties. + Room type 3 exists for all companies and for all properties. + Search for property belonging to a different company than + the 2nd room type created. + The method should return 3rd room type. + + PRE: - room type r1 exists + - room_type1 has code c1 + - room_type1 has property pms_property1 + - pms_property1 have the company company1 + - room_type1 has no company + - room type r2 exists + - room_type2 has code c1 + - room_type2 has no properties + - room_type2 has company company1 + - room type room_type3 exists + - room_type3 has code c1 + - room_type3 has no properties + - room_type3 has no company + ACT: - search room type with code c1 and pms_property3 + - pms_property3 have company2 + POST: - room_type3 room type found + """ + # ARRANGE + # room_type1 + self.env["pms.room.type"].create( + { + "name": "Room type 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + # room_type2 + self.env["pms.room.type"].create( + { + "name": "Room type 2", + "default_code": "c1", + "pms_property_ids": False, + "company_id": self.company1.id, + "class_id": self.room_type_class1.id, + } + ) + room_type3 = self.env["pms.room.type"].create( + { + "name": "Room type 3", + "default_code": "c1", + "pms_property_ids": False, + "company_id": False, + "class_id": self.room_type_class1.id, + } + ) + + # ACT + room_types = self.env["pms.room.type"].get_room_types_by_property( + self.pms_property3.id, "c1" + ) + + # ASSERT + self.assertEqual(room_types.id, room_type3.id, "Expected room type not found") + + def test_rooom_type_creation_inconsistency_class(self): + """ + Create a rooom type class belonging to one property. + Create a room type belonging to another property. + Room type creation should fail. + """ + # ARRANGE + room_type_class = self.env["pms.room.type.class"].create( + { + "name": "Room Type Class", + "default_code": "ROOM", + "pms_property_ids": [ + (4, self.pms_property2.id), + ], + }, + ) + # ACT & ASSERT + with self.assertRaises( + UserError, msg="Room Type has been created and it shouldn't" + ): + room_type1 = self.env["pms.room.type"].create( + { + "name": "Room Type", + "default_code": "c1", + "class_id": room_type_class.id, + "pms_property_ids": [ + (4, self.pms_property2.id), + ], + } + ) + room_type1.pms_property_ids = [(4, self.pms_property1.id)] + + def test_rooom_type_creation_consistency_class(self): + """ + Create a rooom type class belonging to one property. + Create a room type belonging to same property. + Room type creation should be successful. + """ + # ARRANGE + room_type_class = self.env["pms.room.type.class"].create( + { + "name": "Room Type Class", + "default_code": "ROOM", + "pms_property_ids": [ + (4, self.pms_property2.id), + ], + }, + ) + # ACT + new_room_type = self.env["pms.room.type"].create( + { + "name": "Room Type", + "default_code": "c1", + "class_id": room_type_class.id, + "pms_property_ids": [ + (4, self.pms_property2.id), + ], + } + ) + # ASSERT + self.assertTrue(new_room_type.id, "Room type creation should be successful.") + + def test_check_board_service_property_integrity(self): + # ARRANGE + room_type = self.env["pms.room.type"].create( + { + "name": "Room Type", + "default_code": "Type1", + "pms_property_ids": self.pms_property1, + "class_id": self.room_type_class1.id, + } + ) + board_service = self.env["pms.board.service"].create( + { + "name": "Board service 1", + "default_code": "c1", + "pms_property_ids": self.pms_property1, + } + ) + # ACT & ASSERT + with self.assertRaises(UserError, msg="Board service created and shouldn't."): + self.env["pms.board.service.room.type"].create( + { + "pms_board_service_id": board_service.id, + "pms_room_type_id": room_type.id, + "pms_property_id": self.pms_property2.id, + } + ) + + def test_check_amenities_property_integrity(self): + self.amenity1 = self.env["pms.amenity"].create( + {"name": "Amenity", "pms_property_ids": self.pms_property1} + ) + # ACT & ASSERT + with self.assertRaises( + UserError, + msg="Shouldn't create room type with amenities belonging to other properties", + ): + self.env["pms.room.type"].create( + { + "name": "Room Type", + "default_code": "Type1", + "class_id": self.room_type_class1.id, + "pms_property_ids": [self.pms_property2.id], + "room_amenity_ids": [self.amenity1.id], + } + ) + + def test_room_type_creation_consistency_amenity(self): + """ + Create an amenity belonging to one property. + Create a room type belonging to same property. + Room type creation should be successful. + """ + # ARRANGE + self.amenity1 = self.env["pms.amenity"].create( + {"name": "Amenity", "pms_property_ids": self.pms_property1} + ) + # ACT + new_room_type = self.env["pms.room.type"].create( + { + "name": "Room Type", + "default_code": "Type1", + "class_id": self.room_type_class1.id, + "pms_property_ids": [self.pms_property1.id], + "room_amenity_ids": [self.amenity1.id], + } + ) + # ASSERT + self.assertTrue(new_room_type.id, "Room type creation should be successful.") diff --git a/pms/tests/test_pms_room_type_class.py b/pms/tests/test_pms_room_type_class.py new file mode 100644 index 0000000000..b41033bd2f --- /dev/null +++ b/pms/tests/test_pms_room_type_class.py @@ -0,0 +1,383 @@ +# Copyright 2021 Eric Antones +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError + +from .common import TestPms + + +class TestRoomTypeClass(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company2 = cls.env["res.company"].create( + { + "name": "Company 2", + } + ) + cls.pms_property3 = cls.env["pms.property"].create( + { + "name": "Property 3", + "company_id": cls.company2.id, + "default_pricelist_id": cls.pricelist1.id, + } + ) + + # external integrity + def test_external_case_01(self): + """ + Check that a room type class cannot be created with an existing default_code + in the same property. + ---------- + A room type class is created with the default_code = 'c1' in property pms_property1. + Then try to create another room type class in the same property with the same code, + but this should throw a ValidationError. + """ + # ARRANGE + # room_type_class1 + self.env["pms.room.type.class"].create( + { + "name": "Room type class cl1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="The room type class has been created and it shouldn't" + ): + # room_type_class2 + self.env["pms.room.type.class"].create( + { + "name": "Room type class cl2", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + + def test_external_case_02(self): + """ + Check that a room type class cannot be created with an existing default_code + in the same property. + ---------- + A room type class is created with the default_code = 'c1' in property pms_property1. + Then try to create another room type class with the same code in 3 properties and one + of them is the same property in which the other room type class was + created(pms_property1), but this should throw a ValidationError. + """ + # ARRANGE + self.pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + # room_type_class1 + self.env["pms.room.type.class"].create( + { + "name": "Room type class 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="The room type class has been created and it shouldn't" + ): + # room_type_class2 + self.env["pms.room.type.class"].create( + { + "name": "Room type class cl2", + "default_code": "c1", + "pms_property_ids": [ + ( + 6, + 0, + [ + self.pms_property1.id, + self.pms_property2.id, + self.pms_property3.id, + ], + ) + ], + } + ) + + def test_single_case_01(self): + """ + Check that the room type class was created correctly and that + it is in the property in which it was searched through its default code. + ----------- + Create a room type class with default code as 'c1' for properties 1 and 3 + (different companies), save the value returned by the get_unique_by_property_code() + method, passing property1 and default_code 'c1' as parameters. It is checked + that the id of the room type class created and the id of the record returned by the + method match. + """ + # ARRANGE + room_type_class1 = self.env["pms.room.type.class"].create( + { + "name": "Room type class 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property3.id]) + ], + } + ) + + # ACT + room_type_classes = self.env["pms.room.type.class"].get_unique_by_property_code( + self.pms_property1.id, "c1" + ) + + # ASSERT + self.assertEqual( + room_type_classes.id, + room_type_class1.id, + "Expected room type class not found", + ) + + def test_single_case_02(self): + """ + Check that the room type class was created correctly and that + it is in the property in which it was searched through its default code. + ----------- + Create a room type class with default code as 'c1' for properties 1 and 3 + (same company), save the value returned by the get_unique_by_property_code() + method, passing property1 and default_code 'c1' as parameters. It is checked + that the id of the room type class created and the id of the record returned by the + method match. + """ + # ARRANGE + self.pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + cl1 = self.env["pms.room.type.class"].create( + { + "name": "Room type class 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property2.id]) + ], + } + ) + + # ACT + room_type_classes = self.env["pms.room.type.class"].get_unique_by_property_code( + self.pms_property1.id, "c1" + ) + + # ASSERT + self.assertEqual( + room_type_classes.id, cl1.id, "Expected room type class not found" + ) + + def test_single_case_03(self): + """ + Check that a room type class created for a property is not + found in another property with a different company. + ----------- + A room type class is created with default_code 'c1' for properties + 1 and 2. It is searched through get_unique_by_property_code() + passing it as parameters 'c1' and property 3 (from a different + company than 1 and 2). It is verified that that room type class + does not exist in that property. + """ + # ARRANGE + self.pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + # room_type_class1 + self.env["pms.room.type.class"].create( + { + "name": "Room type class 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property2.id]) + ], + } + ) + + # ACT + room_type_classes = self.env["pms.room.type.class"].get_unique_by_property_code( + self.pms_property3.id, "c1" + ) + + # ASSERT + self.assertFalse( + room_type_classes, "Room type class found but it should not have found any" + ) + + def test_single_case_04(self): + """ + Check that a room type class with properties = False + (all properties) is found by searching for it in one + of the properties. + -------------- + A room type is created with default code = 'c1' and with + pms_property_ids = False. The room_type_class with + code 'c1' in property 1 is searched through the + get_unique_by_property_code() method and it is verified + that the returned value is correct. + """ + # ARRANGE + room_type_class1 = self.env["pms.room.type.class"].create( + { + "name": "Room type class 1", + "default_code": "c1", + "pms_property_ids": False, + } + ) + + # ACT + room_type_classes = self.env["pms.room.type.class"].get_unique_by_property_code( + self.pms_property1.id, "c1" + ) + + # ASSERT + self.assertEqual( + room_type_classes.id, + room_type_class1.id, + "Expected room type class not found", + ) + + # tests with more than one room type class + def test_multiple_case_01(self): + """ + Check that a room type class can be created with the same + code as another when one of them has pms_property_ids = False + ------------ + A room type class is created with code 'c1' for properties 1 and 3. + Another room type class is created with code 'c1' and the properties + set to False. The room_type with code 'c1' in property 1 is + searched through the get_unique_by_property_code() method and it is + verified that the returned value is correct. + """ + # ARRANGE + room_type_class1 = self.env["pms.room.type.class"].create( + { + "name": "Room type class 1", + "default_code": "c1", + "pms_property_ids": [ + (6, 0, [self.pms_property1.id, self.pms_property3.id]) + ], + } + ) + # room_type_class2 + self.env["pms.room.type.class"].create( + { + "name": "Room type class cl2", + "default_code": "c1", + "pms_property_ids": False, + } + ) + + # ACT + room_type_classes = self.env["pms.room.type.class"].get_unique_by_property_code( + self.pms_property1.id, "c1" + ) + + # ASSERT + self.assertEqual( + room_type_classes.id, + room_type_class1.id, + "Expected room type class not found", + ) + + def test_multiple_case_02(self): + """ + Check that a room type class can be created with the same + code as another when one of them has pms_property_ids = False + ---------- + A room type class is created with code 'c1' for property 1(company1). + Another room type class is created with code 'c1' and the + properties set to False. Then the room_type with code 'c1' + in property 2(company1) is searched through the + get_unique_by_property_code() method and the result is checked. + """ + # ARRANGE + self.pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + # room_type_class1 + self.env["pms.room.type.class"].create( + { + "name": "Room type class 1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + room_type_class2 = self.env["pms.room.type.class"].create( + { + "name": "Room type class cl2", + "default_code": "c1", + "pms_property_ids": False, + } + ) + + # ACT + room_type_classes = self.env["pms.room.type.class"].get_unique_by_property_code( + self.pms_property2.id, "c1" + ) + + # ASSERT + self.assertEqual( + room_type_classes.id, + room_type_class2.id, + "Expected room type class not found", + ) + + def test_multiple_case_03(self): + """ + Check that a room type class can be created with the same + code as another when one of them has pms_property_ids = False + ---------- + A room type class is created with code 'c1' for property 1(company1). + Another room type class is created with code 'c1' and the + properties set to False. Then the room_type with code 'c1' + in property 3(company2) is searched through the + get_unique_by_property_code() method and the result is checked. + """ + # ARRANGE + # room_type_class1 + self.env["pms.room.type.class"].create( + { + "name": "Room type class cl1", + "default_code": "c1", + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + room_type_class2 = self.env["pms.room.type.class"].create( + { + "name": "Room type class cl2", + "default_code": "c1", + "pms_property_ids": False, + } + ) + + # ACT + room_type_classes = self.env["pms.room.type.class"].get_unique_by_property_code( + self.pms_property3.id, "c1" + ) + + # ASSERT + self.assertEqual( + room_type_classes.id, + room_type_class2.id, + "Expected room type class not found", + ) diff --git a/pms/tests/test_pms_sale_channel.py b/pms/tests/test_pms_sale_channel.py new file mode 100644 index 0000000000..fc13ff8132 --- /dev/null +++ b/pms/tests/test_pms_sale_channel.py @@ -0,0 +1,119 @@ +import datetime + +from odoo.exceptions import ValidationError + +from .common import TestPms + + +class TestPmsSaleChannel(TestPms): + def test_reservation_with_invalid_agency(self): + """ + Reservation with an invalid agency cannot be created. + Create a partner that is not an agency and create + a reservation with that partner as an agency. + """ + # ARRANGE + PmsReservation = self.env["pms.reservation"] + not_agency = self.env["res.partner"].create( + {"name": "partner1", "is_agency": False} + ) + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="Reservation with an invalid agency cannot be created." + ): + PmsReservation.create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "agency_id": not_agency.id, + "pms_property_id": self.pms_property1.id, + } + ) + + def test_reservation_with_valid_agency(self): + """ + Reservation with a valid agency must be created. + Create a partner that is an agency and create + a reservation with that partner as an agency can be created. + """ + # ARRANGE + PmsReservation = self.env["pms.reservation"] + PmsSaleChannel = self.env["pms.sale.channel"] + sale_channel1 = PmsSaleChannel.create( + {"name": "Test Indirect", "channel_type": "indirect"} + ) + # ACT + agency1 = self.env["res.partner"].create( + { + "name": "partner1", + "is_agency": True, + "sale_channel_id": sale_channel1.id, + } + ) + reservation1 = PmsReservation.create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "agency_id": agency1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": sale_channel1.id, + } + ) + + # ASSERT + self.assertEqual( + reservation1.agency_id.is_agency, + True, + "Reservation with a valid agency should be created.", + ) + + def test_create_agency_with_sale_channel_indirect(self): + """ + Agency should be created as partner setted as 'agency' + and its sale channel as 'indirect'. + """ + # ARRANGE + PmsSaleChannel = self.env["pms.sale.channel"] + saleChannel1 = PmsSaleChannel.create({"channel_type": "indirect"}) + # ACT + agency1 = self.env["res.partner"].create( + {"name": "example", "is_agency": True, "sale_channel_id": saleChannel1.id} + ) + # ASSERT + self.assertEqual( + agency1.sale_channel_id.channel_type, + "indirect", + "An agency should be an indirect channel.", + ) + + def test_create_agency_with_sale_channel_direct(self): + """ + Agency shouldnt be created as partner setted as 'agency' + and its sale channel as 'direct'. + """ + # ARRANGE + PmsSaleChannel = self.env["pms.sale.channel"] + saleChannel1 = PmsSaleChannel.create({"channel_type": "direct"}) + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="An agency should be an indirect channel." + ): + self.env["res.partner"].create( + { + "name": "example", + "is_agency": True, + "sale_channel_id": saleChannel1.id, + } + ) + + def test_create_agency_without_sale_channel(self): + """ + Agency creation should fails if there's no sale channel. + """ + # ARRANGE & ACT & ASSERT + with self.assertRaises( + ValidationError, msg="Agency should not be created without sale channel." + ): + self.env["res.partner"].create( + {"name": "example", "is_agency": True, "sale_channel_id": None} + ) diff --git a/pms/tests/test_pms_service.py b/pms/tests/test_pms_service.py new file mode 100644 index 0000000000..b8e710c414 --- /dev/null +++ b/pms/tests/test_pms_service.py @@ -0,0 +1,1058 @@ +import datetime + +from freezegun import freeze_time + +from odoo import fields + +from .common import TestPms + + +class TestPmsService(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + # create room type + cls.room_type_double = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + } + ) + # create rooms + cls.room1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 101", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + "extra_beds_allowed": 1, + } + ) + + cls.room2 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 102", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + "extra_beds_allowed": 1, + } + ) + cls.partner1 = cls.env["res.partner"].create( + { + "firstname": "María", + "lastname": "", + "email": "jaime@example.com", + "birthdate_date": "1983-03-01", + "gender": "male", + } + ) + cls.sale_channel_door = cls.env["pms.sale.channel"].create( + {"name": "Door", "channel_type": "direct"} + ) + cls.sale_channel_phone = cls.env["pms.sale.channel"].create( + {"name": "Phone", "channel_type": "direct"} + ) + cls.sale_channel_mail = cls.env["pms.sale.channel"].create( + {"name": "Mail", "channel_type": "direct"} + ) + + @freeze_time("2002-01-01") + def test_reservation_sale_origin_in_board_service(self): + """ + When a reservation is created with board_service, the sale_channel_origin_id + is indicated in reservation. Therefore, the board_service takes + the sale_channel_origin of its reservation + + Reservation --> sale_channel_origin_id = Door + | + --> service.sale_channel_origin_id? It must be Door + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Test Board Service 1", + "default_code": "CB1", + "pms_property_ids": [self.pms_property1.id], + } + ) + self.board_service_line1 = self.env["pms.board.service.line"].create( + { + "product_id": self.product1.id, + "pms_board_service_id": self.board_service1.id, + "amount": 10, + "adults": True, + } + ) + + self.board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type_double.id, + "pms_board_service_id": self.board_service1.id, + "pms_property_id": self.pms_property1.id, + } + ) + # ACT + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=2), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + "adults": 2, + } + ) + # ASSERT + self.assertEqual( + self.reservation.sale_channel_origin_id, + self.reservation.service_ids.sale_channel_origin_id, + "sale_channel_origin of board_Service must be the same as its reservation", + ) + + @freeze_time("2002-01-11") + def test_change_origin_board_service_not_change_reservation_origin(self): + """ + When you change the sale_channel_origin_id of a board_service in a reservation + that matched the origin of its reservation, if that reservation has reservation_lines + with that sale_channel_id, it doesn't change the origin of reservation + + Reservation --> sale_channel_origin = Door sale_channel_ids = Door + | + --> board_services.sale_channel_origin = Door + + Change board_service origin to Mail + Reservation --> sale_channel_origin = Door sale_channel_ids = {Door, Mail} + | + --> board_services.sale_channel_origin = Mail + + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Test Board Service 1", + "default_code": "CB1", + "pms_property_ids": [self.pms_property1.id], + } + ) + self.board_service_line1 = self.env["pms.board.service.line"].create( + { + "product_id": self.product1.id, + "pms_board_service_id": self.board_service1.id, + "amount": 10, + "adults": True, + } + ) + + self.board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type_double.id, + "pms_board_service_id": self.board_service1.id, + "pms_property_id": self.pms_property1.id, + } + ) + + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + # ACT + self.reservation.service_ids.sale_channel_origin_id = self.sale_channel_mail.id + # ASSERT + self.assertNotEqual( + self.reservation.sale_channel_origin_id, + self.reservation.service_ids.sale_channel_origin_id, + """sale_channel_origin_id mustn't match + with sale_channel_origin_id of its reservation""", + ) + + @freeze_time("2002-01-17") + def test_change_origin_board_service_in_sale_channels(self): + """ + When sale_channel_origin_id of board_service is changed, the sale_channel_ids + of its reservation and folio are recalculated. Check that these calculations are correct + + Reservation --> sale_channel_origin = Door sale_channel_ids = Door + | + ---> board_service.sale_channel_origin = Door + + Change origin of board services to Phone and + check sale_channel_ids of reservation and folio: + Reservation --> sale_channel_origin = Door sale_channel_ids = {Door, Phone} + | + ---> board_service.sale_channel_origin = Phone + + Reservation.folio_id.sale_channel_ids = {Door, Phone} + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Test Board Service 1", + "default_code": "CB1", + "pms_property_ids": [self.pms_property1.id], + } + ) + self.board_service_line1 = self.env["pms.board.service.line"].create( + { + "product_id": self.product1.id, + "pms_board_service_id": self.board_service1.id, + "amount": 10, + "adults": True, + } + ) + + self.board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type_double.id, + "pms_board_service_id": self.board_service1.id, + "pms_property_id": self.pms_property1.id, + } + ) + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + "adults": 2, + } + ) + # ACT + self.reservation.service_ids.sale_channel_origin_id = self.sale_channel_phone + + sale_channel_ids = [ + self.reservation.folio_id.sale_channel_ids, + self.reservation.sale_channel_ids, + ] + + expected_sale_channel_ids = [ + self.sale_channel_door, + self.sale_channel_phone, + ] + # ASSERT + for sale_channel in sale_channel_ids: + with self.subTest(k=sale_channel): + self.assertItemsEqual( + sale_channel, + expected_sale_channel_ids, + "sale_channel_ids must contain sale_channel_origin_id of all board_service", + ) + + @freeze_time("2002-01-19") + def test_change_origin_reservation_change_origin_services(self): + """ + When sale_channel_origin_id of reservation is changed, + sale_channel_origin_id of its services having the same origin + must also be changed + + Reservation ---> sale_channel_origin = Door + | + --> service.sale_channel_origin = Door + + Change sale_channel_origin to Mail, expected results: + Reservation ---> sale_channel_origin = Mail + | + --> service.sale_channel_origin = Mail ----CHECKING THIS--- + + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + } + ) + + self.board_service1 = self.env["pms.board.service"].create( + { + "name": "Test Board Service 1", + "default_code": "CB1", + "pms_property_ids": [self.pms_property1.id], + } + ) + self.board_service_line1 = self.env["pms.board.service.line"].create( + { + "product_id": self.product1.id, + "pms_board_service_id": self.board_service1.id, + "amount": 10, + "adults": True, + } + ) + + self.board_service_room_type1 = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type_double.id, + "pms_board_service_id": self.board_service1.id, + "pms_property_id": self.pms_property1.id, + } + ) + + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=3), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "board_service_room_id": self.board_service_room_type1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + "adults": 2, + } + ) + # ACT + self.reservation.sale_channel_origin_id = self.sale_channel_mail + + # ASSERT + self.assertIn( + self.reservation.sale_channel_origin_id, + self.reservation.service_ids.sale_channel_origin_id, + "sale_channel_origin_id of service must be the same that its reservation ", + ) + + @freeze_time("2002-02-01") + def test_reservation_sale_origin_in_service(self): + """ + When a reservation is created with service, the sale_channel_origin_id + is indicated in reservation. Therefore, the service takes + the sale_channel_origin of its reservation + + Reservation --> sale_channel_origin_id = Door + | + --> service.sale_channel_origin_id? It must be Door + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=2), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + # ACT + self.service1 = self.env["pms.service"].create( + { + "reservation_id": self.reservation.id, + "product_id": self.product1.id, + "is_board_service": False, + } + ) + # ASSERT + self.assertEqual( + self.reservation.sale_channel_origin_id, + self.service1.sale_channel_origin_id, + "sale_channel_origin of service must be the same as its reservation", + ) + + @freeze_time("2002-02-03") + def test_origin_different_in_services_check_sale_channel_ids(self): + """ + Check that sale_channel_ids is calculated well (in folio and + reservation) when a reservation has services from different sale_channels + + Reservation --> sale_channel_origin = Door sale_channel_ids = Door + | + --> service.sale_channel_origin = Door + + Add in reservation another service with sale_channel_origin = Phone, expected results: + + Reservation --> sale_channel_origin = Door sale_channel_ids = Door, Phone + | + --> service[0].sale_channel_origin = Door + | + --> service[1].sale_channel_origin = Phone + + Reservation.folio_id.sale_channels = {Door, Phone} + + Check sale_channel_ids of reservation and its folio + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=2), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + # ACT + self.service1 = self.env["pms.service"].create( + { + "reservation_id": self.reservation.id, + "product_id": self.product1.id, + "is_board_service": False, + } + ) + self.service2 = self.env["pms.service"].create( + { + "reservation_id": self.reservation.id, + "product_id": self.product1.id, + "is_board_service": False, + "sale_channel_origin_id": self.sale_channel_phone.id, + } + ) + + sale_channel_ids = [ + self.reservation.folio_id.sale_channel_ids, + self.reservation.sale_channel_ids, + ] + + expected_sale_channel_ids = self.reservation.service_ids.mapped( + "sale_channel_origin_id" + ) + + # ASSERT + for sale_channel in sale_channel_ids: + with self.subTest(k=sale_channel): + self.assertItemsEqual( + sale_channel, + expected_sale_channel_ids, + "sale_channel_ids must contain sale_channel_id of all board_service_lines", + ) + + @freeze_time("2002-02-16") + def test_change_origin_service_not_change_reservation_origin(self): + """ + When you change the sale_channel_origin_id of a service in a reservation + that matched the origin of its reservation, if that reservation has reservation_lines + with that sale_channel_id, it doesn't change the origin of reservation + + Reservation --> sale_channel_origin = Door + | + --> service.sale_channel_origin = Door + + Change sale_channel_origin of service to Phone, expected results: + + Reservation --> sale_channel_origin = Door ----CHECKING THIS--- + | + --> service.sale_channel_origin = Phone + + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=2), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "reservation_id": self.reservation.id, + "product_id": self.product1.id, + "is_board_service": False, + } + ) + # ACT + self.reservation.service_ids.sale_channel_origin_id = self.sale_channel_phone.id + # ASSERT + self.assertNotEqual( + self.reservation.sale_channel_origin_id, + self.reservation.service_ids.sale_channel_origin_id, + """sale_channel_origin_id mustn't match + with sale_channel_origin_id of its reservation""", + ) + + @freeze_time("2002-02-23") + def test_change_origin_in_services_check_sale_channel_ids(self): + """ + Check that sale_channel_ids is calculated well (in folio and + reservation) when a service of a reservation change its sale_channel_origin + + Reservation --> sale_channel_origin = Door sale_channel_ids = Door + | + --> service.sale_channel_origin = Door + + Change sale_channel_origin of service to Mail, expected results: + + Reservation --> sale_channel_origin = Door + --> sale_channel_ids = Door, Mail -----CHECKING THIS---- + | + --> service.sale_channel_origin = Mail + + Reservation.folio_id.sale_channels = {Door, Mail} -----CHECKING THIS---- + + Check sale_channel_ids of reservation and its folio + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=2), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "reservation_id": self.reservation.id, + "product_id": self.product1.id, + "is_board_service": False, + } + ) + + # ACT + self.service1.sale_channel_origin_id = self.sale_channel_mail + + sale_channel_ids = [ + self.reservation.folio_id.sale_channel_ids.ids, + self.reservation.sale_channel_ids.ids, + ] + + expected_sale_channel_ids = [ + self.sale_channel_door.id, + self.sale_channel_mail.id, + ] + + # ASSERT + for sale_channel in sale_channel_ids: + with self.subTest(k=sale_channel): + self.assertItemsEqual( + sale_channel, + expected_sale_channel_ids, + "sale_channel_ids must contain sale_channel_origin_id of all services", + ) + + @freeze_time("2002-02-25") + def test_change_origin_in_reservation_change_origin_service(self): + """ + Check that when change sale_channel_origin of a reservation, sale_channel_origin + of services that match with the origin changed, change too + + Service --> sale_channel_origin_id = Door sale_channel_ids = {Door, Phone} + | + --> service[0].sale_channel_id = Door + | + --> service[1].sale_channel_id = Phone + + Change service origin to mail, expected results: + Reservation --> sale_channel_origin_id = Mail sale_channel_ids = {Mail, Phone} + | + --> service[0].sale_channel_id = Mail -----------CHECKING THIS--- + | + --> service[1].sale_channel_id = Phone + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=2), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "reservation_id": self.reservation.id, + "product_id": self.product1.id, + "is_board_service": False, + } + ) + self.service2 = self.env["pms.service"].create( + { + "reservation_id": self.reservation.id, + "product_id": self.product1.id, + "is_board_service": False, + "sale_channel_origin_id": self.sale_channel_phone.id, + } + ) + + # ACT + self.reservation.sale_channel_origin_id = self.sale_channel_mail + + # ASSERT + self.assertIn( + self.reservation.sale_channel_origin_id, + self.reservation.service_ids.mapped("sale_channel_origin_id"), + "sale_channel_origin_id of that service should be changed", + ) + + @freeze_time("2002-03-29") + def test_change_origin_in_reservation_no_change_origin_service(self): + """ + Check that when change sale_channel_origin of a reservation, sale_channel_origin + of services that don't match with the origin changed don't change + + Service --> sale_channel_origin_id = Door sale_channel_ids = {Door, Phone} + | + --> service[0].sale_channel_id = Door + | + --> service[1].sale_channel_id = Phone + + Change service origin to mail, expected results: + Reservation --> sale_channel_origin_id = Mail sale_channel_ids = {Mail, Phone} + | + --> service[0].sale_channel_id = Mail + | + --> service[1].sale_channel_id = Phone -----------CHECKING THIS--- + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + + self.reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=2), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "reservation_id": self.reservation.id, + "product_id": self.product1.id, + "is_board_service": False, + } + ) + self.service2 = self.env["pms.service"].create( + { + "reservation_id": self.reservation.id, + "product_id": self.product1.id, + "is_board_service": False, + "sale_channel_origin_id": self.sale_channel_phone.id, + } + ) + + # ACT + self.reservation.sale_channel_origin_id = self.sale_channel_mail + + # ASSERT + self.assertIn( + self.sale_channel_phone, + self.reservation.service_ids.mapped("sale_channel_origin_id"), + "sale_channel_origin_id of that service shouldn't be changed", + ) + + @freeze_time("2002-03-01") + def test_new_service_in_folio_sale_channel_origin(self): + """ + Check that when a service is created within a folio already created, + this service will use the sale_channel_origin_id of the folio as + its sale_channel_origin_id + + Folio ----> sale_channel_origin_id = Door + | + ----> service.sale_channel_origin_id? It must be Door + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": self.partner1.name, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + # ACT + self.service1 = self.env["pms.service"].create( + { + "product_id": self.product1.id, + "is_board_service": False, + "folio_id": self.folio1.id, + } + ) + # ASSERT + self.assertEqual( + self.folio1.sale_channel_origin_id, + self.service1.sale_channel_origin_id, + "Service that is just created must have its folio sale_channel_origin", + ) + + @freeze_time("2002-03-03") + def test_change_origin_folio_change_origin_one_service(self): + """ + Check that when a folio has a service, changing the sale_channel_origin + of folio changes sale_channel_origin of it service + + Folio ----> sale_channel_origin_id = Door + | + ----> service.sale_channel_origin_id = Door + + Change sale_channel_origin of folio to Mail + Folio ----> sale_channel_origin_id = Mail + | + ----> service.sale_channel_origin_id = Mail ---CHECKING THIS--- + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": self.partner1.name, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "product_id": self.product1.id, + "is_board_service": False, + "folio_id": self.folio1.id, + } + ) + # ACT + self.folio1.sale_channel_origin_id = self.sale_channel_mail.id + + # ASSERT + self.assertEqual( + self.folio1.sale_channel_origin_id, + self.service1.sale_channel_origin_id, + "Service must have equal sale_channel_origin than folio", + ) + + @freeze_time("2002-03-05") + def test_change_origin_service_change_origin_folio(self): + """ + When a folio has only one service, when changing the service sale_channel_origin + folio.sale_channel_origin will also change + + Folio ----> sale_channel_origin_id = Door + | + ----> service.sale_channel_origin_id = Door + + Change sale_channel_origin of service to Mail + Folio ----> sale_channel_origin_id = Mail ---CHECKING THIS--- + | + ----> service.sale_channel_origin_id = Mail + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": self.partner1.name, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "product_id": self.product1.id, + "is_board_service": False, + "folio_id": self.folio1.id, + } + ) + # ACT + self.service1.sale_channel_origin_id = self.sale_channel_mail.id + + # ASSERT + self.assertEqual( + self.folio1.sale_channel_origin_id, + self.service1.sale_channel_origin_id, + "Service must have equal sale_channel_origin than folio", + ) + + @freeze_time("2002-03-07") + def test_folio_sale_channels_with_service_different_origins(self): + """ + Check that on a folio with services with differents sale_channel_origin + the sale_channel_ids of folio are calculated well. + In this case sale_channel_ids must be formed by sale_channel_origin of its + services + + Folio ----> sale_channel_origin_id = Door + ----> sale_cahnnel_ids = {Door, Mail} ---CHECKING THIS---- + | + ----> service.sale_channel_origin_id = Door + | + ----> service.sale_channel_origin_id = Mail + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": self.partner1.name, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + # ACT + self.service1 = self.env["pms.service"].create( + { + "product_id": self.product1.id, + "is_board_service": False, + "folio_id": self.folio1.id, + } + ) + self.service2 = self.env["pms.service"].create( + { + "product_id": self.product1.id, + "is_board_service": False, + "folio_id": self.folio1.id, + "sale_channel_origin_id": self.sale_channel_mail.id, + } + ) + + expected_sale_channels = self.folio1.service_ids.mapped( + "sale_channel_origin_id" + ) + + # ASSERT + self.assertEqual( + self.folio1.sale_channel_ids, + expected_sale_channels, + "sale_channel_ids must be the set of sale_channel_origin of its services", + ) + + @freeze_time("2002-03-10") + def test_change_origin_folio_change_origin_service(self): + """ + Check that when a folio has several services with different sale_channel_origin_id + and change sale_channel_origin_id of folio, only changes origin of those services that + match with the sale_channel_origin changed + + Folio ----> sale_channel_origin_id = Door + | + ----> service[0].sale_channel_origin_id = Door + | + ----> service[1].sale_channel_origin_id = Mail + + Change origin of folio to Phone, expected results: + Folio ----> sale_channel_origin_id = Phone + | + ----> service[0].sale_channel_origin_id = Phone ---CHECKIN THIS--- + | + ----> service[1].sale_channel_origin_id = Mail + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": self.partner1.name, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "product_id": self.product1.id, + "is_board_service": False, + "folio_id": self.folio1.id, + } + ) + self.service2 = self.env["pms.service"].create( + { + "product_id": self.product1.id, + "is_board_service": False, + "folio_id": self.folio1.id, + "sale_channel_origin_id": self.sale_channel_mail.id, + } + ) + # ACT + self.folio1.sale_channel_origin_id = self.sale_channel_phone.id + + # ASSERT + self.assertIn( + self.sale_channel_phone, + self.folio1.service_ids.mapped("sale_channel_origin_id"), + "sale_channel_origin_id of that service must be changed", + ) + + @freeze_time("2002-03-13") + def test_change_origin_folio_no_change_origin_service(self): + """ + Check that when a folio has several services with different sale_channel_origin_id + and change sale_channel_origin_id of folio, only changes origin of those services that + match with the sale_channel_origin changed. Then services that didn't initially + match with sale_channel_origin of folio shouldn't have changed + + Folio ----> sale_channel_origin_id = Door + | + ----> service[0].sale_channel_origin_id = Door + | + ----> service[1].sale_channel_origin_id = Mail + + Change origin of folio to Phone, expected results: + Folio ----> sale_channel_origin_id = Phone + | + ----> service[0].sale_channel_origin_id = Phone + | + ----> service[1].sale_channel_origin_id = Mail ---CHECKIN THIS--- + """ + # ARRANGE + self.product1 = self.env["product.product"].create( + { + "name": "Test Product 1", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.folio1 = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_name": self.partner1.name, + "sale_channel_origin_id": self.sale_channel_door.id, + } + ) + + self.service1 = self.env["pms.service"].create( + { + "product_id": self.product1.id, + "is_board_service": False, + "folio_id": self.folio1.id, + } + ) + self.service2 = self.env["pms.service"].create( + { + "product_id": self.product1.id, + "is_board_service": False, + "folio_id": self.folio1.id, + "sale_channel_origin_id": self.sale_channel_mail.id, + } + ) + # ACT + self.folio1.sale_channel_origin_id = self.sale_channel_phone.id + + # ASSERT + self.assertIn( + self.sale_channel_mail, + self.folio1.service_ids.mapped("sale_channel_origin_id"), + "sale_channel_origin_id of that service mustn't be changed", + ) diff --git a/pms/tests/test_pms_simple_invoice.py b/pms/tests/test_pms_simple_invoice.py new file mode 100644 index 0000000000..d4e1dd466b --- /dev/null +++ b/pms/tests/test_pms_simple_invoice.py @@ -0,0 +1,11 @@ +from freezegun import freeze_time + +from odoo.tests.common import SavepointCase + +freeze_time("2000-02-02") + + +class TestPmsInvoiceSimpleInvoice(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() diff --git a/pms/tests/test_pms_tourist_tax.py b/pms/tests/test_pms_tourist_tax.py new file mode 100644 index 0000000000..9a96d47c15 --- /dev/null +++ b/pms/tests/test_pms_tourist_tax.py @@ -0,0 +1,299 @@ +import datetime +import logging + +from freezegun import freeze_time + +from odoo import fields +from odoo.tests.common import tagged + +from .common import TestPms + +_logger = logging.getLogger(__name__) + + +@tagged("tourist_tax") +class TestTouristTaxComputation(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.room_type_double = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + } + ) + + cls.room1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 101", + "room_type_id": cls.room_type_double.id, + "capacity": 2, + "extra_beds_allowed": 1, + } + ) + + cls.partner_adult = cls.env["res.partner"].create( + { + "firstname": "Adult", + "birthdate_date": "1990-01-01", + } + ) + + cls.partner_child = cls.env["res.partner"].create( + { + "firstname": "Child", + "birthdate_date": "2015-01-01", + } + ) + + cls.sale_channel_direct = cls.env["pms.sale.channel"].create( + {"name": "sale channel direct", "channel_type": "direct"} + ) + + @freeze_time("2025-07-01") + def test_tourist_tax_lines_correctly_generated(self): + """ + Test that the tourist tax service lines are correctly computed for 3 nights + with one adult and one child (child is under age and excluded). + """ + self.env["product.product"].create( + { + "name": "Tourist Tax", + "list_price": 2.0, + "is_tourist_tax": True, + "per_person": True, + "tourist_tax_date_start": "06-01", + "tourist_tax_date_end": "09-30", + "tourist_tax_apply_from_night": 1, + "tourist_tax_apply_to_night": 3, + "tourist_tax_min_age": 14, + } + ) + + checkin = fields.Date.today() + checkout = checkin + datetime.timedelta(days=3) + + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_adult.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + "adults": 2, + "checkin_partner_ids": [ + (0, 0, {"partner_id": self.partner_adult.id}), + (0, 0, {"partner_id": self.partner_child.id}), + ], + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + } + ) + + tax_services = reservation.service_ids.filtered( + lambda s: s.product_id.product_tmpl_id.is_tourist_tax + ) + self.assertEqual( + len(tax_services), 1, "Should generate one tourist tax service" + ) + + service = tax_services[0] + self.assertEqual( + len(service.service_line_ids), 3, "Should have 3 nights of tax" + ) + + for line in service.service_line_ids: + self.assertEqual(line.day_qty, 1, "Only adult should be taxed") + self.assertEqual( + line.price_unit, 2.0, "Price should match product list_price" + ) + + @freeze_time("2025-12-15") + def test_tourist_tax_not_applied_outside_period(self): + """ + Test that tourist tax is not applied outside the configured date range. + """ + self.env["product.product"].create( + { + "name": "Tourist Tax", + "list_price": 2.0, + "is_tourist_tax": True, + "per_person": True, + "tourist_tax_date_start": "06-01", + "tourist_tax_date_end": "09-30", + "tourist_tax_apply_from_night": 1, + "tourist_tax_apply_to_night": 3, + "tourist_tax_min_age": 14, + } + ) + + checkin = fields.Date.today() + checkout = checkin + datetime.timedelta(days=2) + + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_adult.id, + "adults": 2, + "sale_channel_origin_id": self.sale_channel_direct.id, + "checkin_partner_ids": [ + (0, 0, {"partner_id": self.partner_adult.id}), + ], + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + } + ) + + tax_services = reservation.service_ids.filtered( + lambda s: s.product_id.product_tmpl_id.is_tourist_tax + ) + + self.assertEqual( + len(tax_services), + 0, + "No tax should be applied outside the defined MM-DD range", + ) + + @freeze_time("2025-07-01") + def test_tourist_tax_stops_after_max_night(self): + """ + Test that tax is not applied beyond the configured max night (apply_to_night = 3). + """ + self.env["product.product"].create( + { + "name": "Tourist Tax", + "list_price": 2.0, + "is_tourist_tax": True, + "per_person": True, + "tourist_tax_date_start": "06-01", + "tourist_tax_date_end": "09-30", + "tourist_tax_apply_from_night": 1, + "tourist_tax_apply_to_night": 3, + "tourist_tax_min_age": 14, + } + ) + + checkin = fields.Date.today() + checkout = checkin + datetime.timedelta(days=5) + + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_adult.id, + "adults": 2, + "sale_channel_origin_id": self.sale_channel_direct.id, + "checkin_partner_ids": [ + (0, 0, {"partner_id": self.partner_adult.id}), + ], + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + } + ) + + tax_services = reservation.service_ids.filtered( + lambda s: s.product_id.product_tmpl_id.is_tourist_tax + ) + + self.assertEqual( + len(tax_services), 1, "Should generate one tourist tax service" + ) + service = tax_services[0] + + self.assertEqual( + len(service.service_line_ids), 3, "Tax should only apply to first 3 nights" + ) + + @freeze_time("2025-06-28") + def test_multiple_tourist_taxes_applied_by_season(self): + """ + Test that two different tourist taxes + (low and high season) apply correctly over 8 nights. + """ + # Arrange + low_season_tax = self.env["product.product"].create( + { + "name": "Tourist Tax Low Season", + "list_price": 1.5, + "is_tourist_tax": True, + "per_person": True, + "tourist_tax_date_start": "01-01", + "tourist_tax_date_end": "06-30", + "tourist_tax_apply_from_night": 1, + "tourist_tax_apply_to_night": 3, + "tourist_tax_min_age": 14, + } + ) + + high_season_tax = self.env["product.product"].create( + { + "name": "Tourist Tax High Season", + "list_price": 2.0, + "is_tourist_tax": True, + "per_person": True, + "tourist_tax_date_start": "07-01", + "tourist_tax_date_end": "09-30", + "tourist_tax_apply_from_night": 1, + "tourist_tax_apply_to_night": 99, + "tourist_tax_min_age": 14, + } + ) + + checkin = fields.Date.today() # 2025-06-28 + checkout = checkin + datetime.timedelta(days=8) # until 2025-07-06 + + reservation = self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner_adult.id, + "adults": 1, + "sale_channel_origin_id": self.sale_channel_direct.id, + "checkin_partner_ids": [ + (0, 0, {"partner_id": self.partner_adult.id}), + ], + "pms_property_id": self.pms_property1.id, + "pricelist_id": self.pricelist1.id, + } + ) + + # Act + tax_services = reservation.service_ids.filtered( + lambda s: s.product_id.product_tmpl_id.is_tourist_tax + ) + # Assert + self.assertEqual( + len(tax_services), + 2, + "Should generate two tourist tax services (low and high season)", + ) + + low = tax_services.filtered(lambda s: s.product_id == low_season_tax) + high = tax_services.filtered(lambda s: s.product_id == high_season_tax) + + self.assertEqual( + len(low.service_line_ids), + 3, + "Low season should apply to first 3 nights (06/28–06/30)", + ) + self.assertEqual( + len(high.service_line_ids), + 5, + "High season should apply to last 5 nights (07/01–07/05)", + ) + + for line in low.service_line_ids: + self.assertEqual(line.day_qty, 1) + self.assertEqual(line.price_unit, 1.5) + + for line in high.service_line_ids: + self.assertEqual(line.day_qty, 1) + self.assertEqual(line.price_unit, 2.0) diff --git a/pms/tests/test_pms_wizard_massive_changes.py b/pms/tests/test_pms_wizard_massive_changes.py new file mode 100644 index 0000000000..5b8d930b87 --- /dev/null +++ b/pms/tests/test_pms_wizard_massive_changes.py @@ -0,0 +1,1315 @@ +import datetime + +from freezegun import freeze_time + +from odoo import fields + +from .common import TestPms + + +class TestPmsWizardMassiveChanges(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.availability_plan1 = cls.env["pms.availability.plan"].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [cls.pricelist1.id])], + } + ) + + # MASSIVE CHANGE WIZARD TESTS ON AVAILABILITY RULES + + def test_num_availability_rules_create(self): + """ + Rules should be created consistently for 1,2,3,4 days + subtests: {1 day -> 1 rule, n days -> n rules} + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + for days in [0, 1, 2, 3]: + with self.subTest(k=days): + num_exp_rules_to_create = days + 1 + # ACT + self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "availability_plan", + "availability_plan_ids": [(6, 0, [self.availability_plan1.id])], + "start_date": fields.date.today(), + "end_date": fields.date.today() + datetime.timedelta(days=days), + "room_type_ids": [(6, 0, [room_type_double.id])], + "pms_property_ids": [self.pms_property1.id], + } + ).apply_massive_changes() + # ASSERT + self.assertEqual( + len(self.availability_plan1.rule_ids), + num_exp_rules_to_create, + "the number of rules created should contains all the " + "days between start and finish (both included)", + ) + + def test_num_availability_rules_create_no_room_type(self): + """ + Rules should be created consistently for all rooms & days. + (days * num rooom types) + Create rules for 4 days and for all room types. + """ + # ARRANGE + date_from = fields.date.today() + date_to = fields.date.today() + datetime.timedelta(days=3) + + num_room_types = self.env["pms.room.type"].search_count( + [ + "|", + ("pms_property_ids", "=", False), + ("pms_property_ids", "in", self.pms_property1.id), + ] + ) + num_exp_rules_to_create = ((date_to - date_from).days + 1) * num_room_types + + # ACT + self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "availability_plan", + "availability_plan_ids": [(6, 0, [self.availability_plan1.id])], + "start_date": date_from, + "end_date": date_to, + "pms_property_ids": [self.pms_property1.id], + } + ).apply_massive_changes() + + # ASSERT + self.assertEqual( + len(self.availability_plan1.rule_ids), + num_exp_rules_to_create, + "the number of rules created by the wizard should consider all " + "room types", + ) + + def test_value_availability_rules_create(self): + """ + The value of the rules created is setted properly. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + date_from = fields.date.today() + date_to = fields.date.today() + vals = { + "massive_changes_on": "availability_plan", + "availability_plan_ids": [(6, 0, [self.availability_plan1.id])], + "start_date": date_from, + "end_date": date_to, + "room_type_ids": [(6, 0, [room_type_double.id])], + "quota": 50, + "max_avail": 5, + "min_stay": 10, + "min_stay_arrival": 10, + "max_stay": 10, + "max_stay_arrival": 10, + "closed": True, + "closed_arrival": True, + "closed_departure": True, + "pms_property_ids": [self.pms_property1.id], + } + # ACT + self.env["pms.massive.changes.wizard"].create(vals).apply_massive_changes() + # ASSERT + del vals["massive_changes_on"] + del vals["availability_plan_ids"] + del vals["start_date"] + del vals["end_date"] + del vals["room_type_ids"] + del vals["pms_property_ids"] + for key in vals: + with self.subTest(k=key): + self.assertEqual( + self.availability_plan1.rule_ids[0][key], + vals[key], + "The value of " + key + " is not correctly established", + ) + + @freeze_time("1980-12-01") + def test_day_of_week_availability_rules_create(self): + """ + Rules for each day of week should be created. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + test_case_week_days = [ + [1, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1], + ] + date_from = fields.date.today() + date_to = fields.date.today() + datetime.timedelta(days=6) + + wizard = self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "availability_plan", + "availability_plan_ids": [(6, 0, [self.availability_plan1.id])], + "room_type_ids": [(6, 0, [room_type_double.id])], + "start_date": date_from, + "end_date": date_to, + "pms_property_ids": [self.pms_property1.id], + } + ) + + for index, test_case in enumerate(test_case_week_days): + with self.subTest(k=test_case): + # ARRANGE + wizard.write( + { + "apply_on_monday": test_case[0], + "apply_on_tuesday": test_case[1], + "apply_on_wednesday": test_case[2], + "apply_on_thursday": test_case[3], + "apply_on_friday": test_case[4], + "apply_on_saturday": test_case[5], + "apply_on_sunday": test_case[6], + } + ) + # ACT + wizard.apply_massive_changes() + availability_rules = self.availability_plan1.rule_ids.sorted( + key=lambda s: s.date + ) + # ASSERT + self.assertTrue( + availability_rules[index].date.timetuple()[6] == index + and test_case[index], + "Rule not created on correct day of week.", + ) + + def test_no_overwrite_values_not_setted(self): + """ + A rule value shouldnt overwrite with the default values + another rules for the same day and room type. + Create a rule with quota and another rule for the same date with max + avail. Should not overwrite quota. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + date = fields.date.today() + initial_quota = 20 + self.env["pms.availability.plan.rule"].create( + { + "availability_plan_id": self.availability_plan1.id, + "room_type_id": room_type_double.id, + "date": date, + "quota": initial_quota, + "pms_property_id": self.pms_property1.id, + } + ) + vals_wizard = { + "massive_changes_on": "availability_plan", + "availability_plan_ids": [(6, 0, [self.availability_plan1.id])], + "start_date": date, + "end_date": date, + "room_type_ids": [(6, 0, [room_type_double.id])], + "apply_max_avail": True, + "max_avail": 2, + "pms_property_ids": [self.pms_property1.id], + } + # ACT + self.env["pms.massive.changes.wizard"].create( + vals_wizard + ).apply_massive_changes() + # ASSERT + self.assertEqual( + self.availability_plan1.rule_ids[0].quota, + initial_quota, + "A rule value shouldnt overwrite with the default values " + "another rules for the same day and room type", + ) + + def test_several_availability_plans(self): + """ + If several availability plans are set, the wizard should create as + many rules as availability plans. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + availability_plan2 = self.env["pms.availability.plan"].create( + { + "name": "Second availability plan for TEST", + "pms_pricelist_ids": [self.pricelist1.id], + } + ) + expected_av_plans = [ + self.availability_plan1.id, + availability_plan2.id, + ] + date_from = fields.date.today() + date_to = fields.date.today() + vals_wizard = { + "massive_changes_on": "availability_plan", + "availability_plan_ids": [ + ( + 6, + 0, + [ + self.availability_plan1.id, + availability_plan2.id, + ], + ) + ], + "room_type_ids": [(6, 0, [room_type_double.id])], + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + } + # ACT + self.env["pms.massive.changes.wizard"].create( + vals_wizard + ).apply_massive_changes() + # ASSERT + self.assertEqual( + set(expected_av_plans), + set( + self.env["pms.availability.plan.rule"] + .search([("room_type_id", "=", room_type_double.id)]) + .mapped("availability_plan_id") + .ids + ), + "The wizard should create as many rules as availability plans given.", + ) + + def test_several_room_types_availability_plan(self): + """ + If several room types are set, the wizard should create as + many rules as room types. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + room_type_single = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Single Test", + "default_code": "SNG_Test", + "class_id": self.room_type_class1.id, + } + ) + expected_room_types = [ + room_type_double.id, + room_type_single.id, + ] + date_from = fields.date.today() + date_to = fields.date.today() + vals_wizard = { + "massive_changes_on": "availability_plan", + "availability_plan_ids": [(6, 0, [self.availability_plan1.id])], + "room_type_ids": [ + ( + 6, + 0, + [room_type_double.id, room_type_single.id], + ) + ], + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + } + # ACT + self.env["pms.massive.changes.wizard"].create( + vals_wizard + ).apply_massive_changes() + # ASSERT + self.assertEqual( + set(expected_room_types), + set( + self.env["pms.availability.plan.rule"] + .search([("availability_plan_id", "=", self.availability_plan1.id)]) + .mapped("room_type_id") + .ids + ), + "The wizard should create as many rules as room types given.", + ) + + def test_several_properties_availability_plan(self): + """ + If several properties are set, the wizard should create as + many rules as properties. + """ + # ARRANGE + pms_property2 = self.env["pms.property"].create( + { + "name": "MY 2nd PMS TEST", + "company_id": self.env.ref("base.main_company").id, + } + ) + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + room_type_double.pms_property_ids = [ + (6, 0, [self.pms_property1.id, pms_property2.id]) + ] + expected_properties = [ + self.pms_property1.id, + pms_property2.id, + ] + date_from = fields.date.today() + date_to = fields.date.today() + vals_wizard = { + "massive_changes_on": "availability_plan", + "availability_plan_ids": [(6, 0, [self.availability_plan1.id])], + "room_type_ids": [(6, 0, [room_type_double.id])], + "pms_property_ids": [(6, 0, [self.pms_property1.id, pms_property2.id])], + "start_date": date_from, + "end_date": date_to, + } + # ACT + self.env["pms.massive.changes.wizard"].create( + vals_wizard + ).apply_massive_changes() + # ASSERT + self.assertEqual( + set(expected_properties), + set( + self.env["pms.availability.plan.rule"] + .search([("availability_plan_id", "=", self.availability_plan1.id)]) + .mapped("pms_property_id") + .ids + ), + "The wizard should create as many rules as properties given.", + ) + + def test_create_rule_existing_previous(self): + """ + If there's a previous rule with some value and new values are set + that contains date of previuos value should overwrite the value. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + date = fields.date.today() + initial_quota = 20 + self.env["pms.availability.plan.rule"].create( + { + "availability_plan_id": self.availability_plan1.id, + "room_type_id": room_type_double.id, + "date": date, + "quota": initial_quota, + "pms_property_id": self.pms_property1.id, + } + ) + vals_wizard = { + "massive_changes_on": "availability_plan", + "availability_plan_ids": [(6, 0, [self.availability_plan1.id])], + "start_date": date, + "end_date": fields.date.today() + datetime.timedelta(days=1), + "room_type_ids": [(6, 0, [room_type_double.id])], + "apply_quota": True, + "quota": 20, + "pms_property_ids": [self.pms_property1.id], + } + + # ACT + self.env["pms.massive.changes.wizard"].create( + vals_wizard + ).apply_massive_changes() + + # ASSERT + self.assertEqual( + self.availability_plan1.rule_ids[0].quota, + initial_quota, + "A rule value shouldnt overwrite with the default values " + "another rules for the same day and room type", + ) + + # MASSIVE CHANGE WIZARD TESTS ON PRICELIST ITEMS + + def test_pricelist_items_create(self): + """ + Pricelist items should be created consistently for 1,2,3,4 days + subtests: {1 day -> 1 pricelist item, n days -> n pricelist items} + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + for days in [0, 1, 2, 3]: + with self.subTest(k=days): + # ARRANGE + num_exp_items_to_create = days + 1 + self.pricelist1.item_ids = False + # ACT + self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "start_date": fields.date.today(), + "end_date": fields.date.today() + datetime.timedelta(days=days), + "room_type_ids": [(6, 0, [room_type_double.id])], + "pms_property_ids": [self.pms_property1.id], + "price": 20, + } + ).apply_massive_changes() + # ASSERT + self.assertEqual( + len(self.pricelist1.item_ids if self.pricelist1.item_ids else []), + num_exp_items_to_create, + "the number of rules created by the wizard should include all the " + "days between start and finish (both included)", + ) + + def test_num_pricelist_items_create_no_room_type(self): + """ + Pricelist items should be created consistently for all rooms & days. + (days * num rooom types) + Create pricelist item for 4 days and for all room types. + """ + # ARRANGE + date_from = fields.date.today() + date_to = fields.date.today() + datetime.timedelta(days=3) + num_room_types = self.env["pms.room.type"].search_count( + [ + "|", + ("pms_property_ids", "=", False), + ("pms_property_ids", "in", self.pms_property1.id), + ] + ) + num_exp_items_to_create = ((date_to - date_from).days + 1) * num_room_types + # ACT + self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "start_date": date_from, + "end_date": date_to, + "pms_property_ids": [self.pms_property1.id], + "price": 20, + } + ).apply_massive_changes() + # ASSERT + self.assertEqual( + len(self.pricelist1.item_ids), + num_exp_items_to_create, + "the number of rules created by the wizard should consider all " + "room types when one is not applied", + ) + + def test_value_pricelist_items_create(self): + """ + The value of the rules created is setted properly. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + date_from = fields.date.today() + date_to = fields.date.today() + price = 20 + min_quantity = 3 + vals = { + "pricelist_id": self.pricelist1, + "date_start": date_from, + "date_end": date_to, + "compute_price": "fixed", + "applied_on": "0_product_variant", + "product_id": room_type_double.product_id, + "fixed_price": price, + "min_quantity": min_quantity, + } + # ACT + self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "start_date": date_from, + "end_date": date_to, + "room_type_ids": [(6, 0, [room_type_double.id])], + "price": price, + "min_quantity": min_quantity, + "pms_property_ids": [self.pms_property1.id], + } + ).apply_massive_changes() + vals["date_start_consumption"] = date_from + vals["date_end_consumption"] = date_to + del vals["date_start"] + del vals["date_end"] + # ASSERT + for key in vals: + with self.subTest(k=key): + self.assertEqual( + self.pricelist1.item_ids[0][key], + vals[key], + "The value of " + key + " is not correctly established", + ) + + @freeze_time("1980-12-01") + def test_day_of_week_pricelist_items_create(self): + """ + Pricelist items for each day of week should be created. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + test_case_week_days = [ + [1, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1], + ] + date_from = fields.date.today() + date_to = date_from + datetime.timedelta(days=6) + wizard = self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "room_type_ids": [(6, 0, [room_type_double.id])], + "start_date": date_from, + "end_date": date_to, + "pms_property_ids": [self.pms_property1.id], + "price": 20, + } + ) + for index, test_case in enumerate(test_case_week_days): + with self.subTest(k=test_case): + # ARRANGE + wizard.write( + { + "apply_on_monday": test_case[0], + "apply_on_tuesday": test_case[1], + "apply_on_wednesday": test_case[2], + "apply_on_thursday": test_case[3], + "apply_on_friday": test_case[4], + "apply_on_saturday": test_case[5], + "apply_on_sunday": test_case[6], + } + ) + self.pricelist1.item_ids = False + # ACT + wizard.apply_massive_changes() + pricelist_items = self.pricelist1.item_ids.sorted( + key=lambda s: s.date_start_consumption + ) + # ASSERT + self.assertTrue( + pricelist_items[index].date_start_consumption.timetuple()[6] + == index + and test_case[index], + "Rule not created on correct day of week", + ) + + def test_several_pricelists(self): + """ + If several pricelist are set, the wizard should create as + many pricelist items as pricelists. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + pricelist2 = self.env["product.pricelist"].create( + { + "name": "test pricelist 2", + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + expected_pricelists = [self.pricelist1.id, pricelist2.id] + date_from = fields.date.today() + date_to = fields.date.today() + vals_wizard = { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id, pricelist2.id])], + "room_type_ids": [(6, 0, [room_type_double.id])], + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + "price": 20, + } + # ACT + self.env["pms.massive.changes.wizard"].create( + vals_wizard + ).apply_massive_changes() + # ASSERT + self.assertEqual( + set(expected_pricelists), + set( + self.env["product.pricelist.item"] + .search([("product_id", "=", room_type_double.product_id.id)]) + .mapped("pricelist_id") + .ids + ), + "The wizard should create as many items as pricelists given.", + ) + + def test_several_room_types_pricelist(self): + """ + If several room types are set, the wizard should create as + many pricelist items as room types. + """ + # ARRANGE + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + room_type_single = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Single Test", + "default_code": "SNG_Test", + "class_id": self.room_type_class1.id, + } + ) + date_from = fields.date.today() + date_to = fields.date.today() + expected_product_ids = [ + room_type_double.product_id.id, + room_type_single.product_id.id, + ] + vals_wizard = { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "room_type_ids": [ + ( + 6, + 0, + [room_type_double.id, room_type_single.id], + ) + ], + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + "price": 20, + } + # ACT + self.env["pms.massive.changes.wizard"].create( + vals_wizard + ).apply_massive_changes() + # ASSERT + self.assertEqual( + set(expected_product_ids), + set( + self.env["product.pricelist.item"] + .search([("pricelist_id", "=", self.pricelist1.id)]) + .mapped("product_id") + .ids + ), + "The wizard should create as many items as room types given.", + ) + + def test_one_board_service_room_type_no_board_service(self): + """ + Call to wizard with one board service room type and no + board service. + The wizard must create as many pricelist items as there + are services on the given board service. + """ + # ARRANGE + room_type_single = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + board_service_only_breakfast = self.env["pms.board.service"].create( + { + "name": "Test Only Breakfast", + "default_code": "CB1", + "pms_property_ids": [self.pms_property1.id], + } + ) + service_breakfast = self.env["product.product"].create( + {"name": "Test Breakfast"} + ) + board_service_single = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type_single.id, + "pms_board_service_id": board_service_only_breakfast.id, + "pms_property_id": self.pms_property1.id, + } + ) + board_service_line_single_1 = self.env["pms.board.service.line"].create( + { + "product_id": service_breakfast.id, + "pms_board_service_id": board_service_only_breakfast.id, + "adults": True, + } + ) + date_from = fields.date.today() + date_to = fields.date.today() + wizard_result = self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "apply_pricelists_on": "board_services", + "board_service_room_type_ids": [ + ( + 6, + 0, + [board_service_single.id], + ) + ], + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + "date_types": "consumption_dates", + } + ) + # ACT + wizard_result.apply_massive_changes() + + items_created = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", self.pricelist1.id), + ("pms_property_ids", "=", self.pms_property1.id), + ("product_id", "=", board_service_line_single_1.product_id.id), + ] + ) + # ASSERT + self.assertIn( + service_breakfast, + items_created.mapped("product_id"), + "The wizard must create as many pricelist items as there " + "are services on the given board service.", + ) + + def test_one_board_service_room_type_with_board_service(self): + """ + Call to wizard with one board service room type and + board service. + The wizard must create one pricelist items with + the board service given. + """ + # ARRANGE + room_type_single = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + board_service_only_breakfast = self.env["pms.board.service"].create( + { + "name": "Test Only Breakfast", + "default_code": "CB1", + "pms_property_ids": [self.pms_property1.id], + } + ) + service_breakfast = self.env["product.product"].create( + {"name": "Test Breakfast"} + ) + board_service_single = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type_single.id, + "pms_board_service_id": board_service_only_breakfast.id, + "pms_property_id": self.pms_property1.id, + } + ) + board_service_line_single_1 = self.env["pms.board.service.line"].create( + { + "product_id": service_breakfast.id, + "pms_board_service_id": board_service_only_breakfast.id, + "adults": True, + } + ) + date_from = fields.date.today() + date_to = fields.date.today() + wizard_result = self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "apply_pricelists_on": "board_services", + "board_service_room_type_ids": [ + ( + 6, + 0, + [board_service_single.id], + ) + ], + "board_service": board_service_line_single_1.product_id.id, + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + "date_types": "consumption_dates", + } + ) + # ACT + wizard_result.apply_massive_changes() + + items_created = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", self.pricelist1.id), + ("pms_property_ids", "=", self.pms_property1.id), + ("product_id", "=", board_service_line_single_1.product_id.id), + ] + ) + # ASSERT + self.assertIn( + service_breakfast, + items_created.mapped("product_id"), + "The wizard must create one pricelist items with " + " the board service given.", + ) + + def test_several_board_service_room_type_no_board_service(self): + """ + Call to wizard with several board service room type and no + board service. + The wizard must create as many pricelist items as there + are services on the given board services. + """ + # ARRANGE + room_type_single = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "SNG_Test", + "class_id": self.room_type_class1.id, + } + ) + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + board_service_only_breakfast = self.env["pms.board.service"].create( + { + "name": "Test Only Breakfast", + "default_code": "CB1", + "pms_property_ids": [self.pms_property1.id], + } + ) + board_service_half_board = self.env["pms.board.service"].create( + { + "name": "Test Half Board", + "default_code": "CB2", + "pms_property_ids": [self.pms_property1.id], + } + ) + service_breakfast = self.env["product.product"].create( + {"name": "Test Breakfast"} + ) + service_dinner = self.env["product.product"].create({"name": "Test Dinner"}) + board_service_single = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type_single.id, + "pms_board_service_id": board_service_only_breakfast.id, + "pms_property_id": self.pms_property1.id, + } + ) + board_service_double = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type_double.id, + "pms_board_service_id": board_service_half_board.id, + "pms_property_id": self.pms_property1.id, + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": service_breakfast.id, + "pms_board_service_id": board_service_only_breakfast.id, + "adults": True, + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": service_breakfast.id, + "pms_board_service_id": board_service_half_board.id, + "adults": True, + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": service_dinner.id, + "pms_board_service_id": board_service_half_board.id, + "adults": True, + } + ) + date_from = fields.date.today() + date_to = fields.date.today() + product_ids_expected = ( + board_service_double.pms_board_service_id.board_service_line_ids.mapped( + "product_id" + ).ids + + board_service_single.pms_board_service_id.board_service_line_ids.mapped( + "product_id" + ).ids + ) + wizard_result = self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "apply_pricelists_on": "board_services", + "board_service_room_type_ids": [ + ( + 6, + 0, + [ + board_service_single.id, + board_service_double.id, + ], + ) + ], + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + "date_types": "consumption_dates", + } + ) + # ACT + wizard_result.apply_massive_changes() + items_created = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", self.pricelist1.id), + ("pms_property_ids", "=", self.pms_property1.id), + ("product_id", "in", product_ids_expected), + ] + ) + # ASSERT + self.assertEqual( + set(product_ids_expected), + set(items_created.mapped("product_id").ids), + "The wizard should create as many pricelist items as there" + " are services on the given board services.", + ) + + def test_several_board_service_room_type_with_board_service(self): + + """ + Call to wizard with several board service room types and + board service. + The wizard must create as many pricelist items as there + are services on the given board services. + """ + # ARRANGE + room_type_single = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "SNG_Test", + "class_id": self.room_type_class1.id, + } + ) + room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": self.room_type_class1.id, + } + ) + board_service_only_breakfast = self.env["pms.board.service"].create( + { + "name": "Test Only Breakfast", + "default_code": "CB1", + "pms_property_ids": [self.pms_property1.id], + } + ) + board_service_half_board = self.env["pms.board.service"].create( + { + "name": "Test Half Board", + "default_code": "CB2", + "pms_property_ids": [self.pms_property1.id], + } + ) + service_breakfast = self.env["product.product"].create( + {"name": "Test Breakfast"} + ) + service_dinner = self.env["product.product"].create({"name": "Test Dinner"}) + board_service_single = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type_single.id, + "pms_board_service_id": board_service_only_breakfast.id, + "pms_property_id": self.pms_property1.id, + } + ) + board_service_double = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": room_type_double.id, + "pms_board_service_id": board_service_half_board.id, + "pms_property_id": self.pms_property1.id, + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": service_breakfast.id, + "pms_board_service_id": board_service_only_breakfast.id, + "adults": True, + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": service_breakfast.id, + "pms_board_service_id": board_service_half_board.id, + "adults": True, + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": service_dinner.id, + "pms_board_service_id": board_service_half_board.id, + "adults": True, + } + ) + date_from = fields.date.today() + date_to = fields.date.today() + board_service_id_double = board_service_double.pms_board_service_id + board_service_id_single = board_service_single.pms_board_service_id + product_ids_expected = list( + set(board_service_id_double.board_service_line_ids.mapped("product_id").ids) + & set( + board_service_id_single.board_service_line_ids.mapped("product_id").ids + ) + ) + wizard_result = self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "apply_pricelists_on": "board_services", + "board_service_room_type_ids": [ + ( + 6, + 0, + [ + board_service_single.id, + board_service_double.id, + ], + ) + ], + "board_service": service_breakfast.id, + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + "date_types": "consumption_dates", + } + ) + # ACT + wizard_result.apply_massive_changes() + + items_created = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", self.pricelist1.id), + ("pms_property_ids", "=", self.pms_property1.id), + ("product_id", "in", product_ids_expected), + ] + ) + # ASSERT + self.assertEqual( + set(product_ids_expected), + set(items_created.mapped("product_id").ids), + "The wizard should create as many pricelist items as there" + " are services on the given board services.", + ) + + def test_service(self): + """ + Call to wizard with one service (product_id) + The wizard must create one pricelist items with + the given service (product_id). + """ + # ARRANGE + service_spa = self.env["product.product"].create({"name": "Test Spa"}) + date_from = fields.date.today() + date_to = fields.date.today() + wizard_result = self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "apply_pricelists_on": "service", + "service": service_spa.id, + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + "date_types": "consumption_dates", + } + ) + # ACT + wizard_result.apply_massive_changes() + items_created = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", self.pricelist1.id), + ("pms_property_ids", "=", self.pms_property1.id), + ("product_id", "=", service_spa.id), + ] + ) + # ASSERT + self.assertIn( + service_spa.id, + items_created.mapped("product_id").ids, + "The wizard should create one pricelist items with" + " the given service (product_id).", + ) + + def test_sale_dates(self): + """ + Call to wizard with one service (product_id) + and dates of SALE + The wizard must create one pricelist items with + the given service (product_id) and dates of SALE. + """ + # ARRANGE + service_spa = self.env["product.product"].create({"name": "Test Spa"}) + date_from = fields.date.today() + date_to = fields.date.today() + wizard_result = self.env["pms.massive.changes.wizard"].create( + { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "apply_pricelists_on": "service", + "service": service_spa.id, + "pms_property_ids": [self.pms_property1.id], + "start_date": date_from, + "end_date": date_to, + "date_types": "sale_dates", + } + ) + # ACT + wizard_result.apply_massive_changes() + items_created = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", self.pricelist1.id), + ("pms_property_ids", "=", self.pms_property1.id), + ("product_id", "=", service_spa.id), + ] + ) + expected_dates = [ + datetime.datetime.combine(date_from, datetime.datetime.min.time()), + datetime.datetime.combine(date_to, datetime.datetime.max.time()), + ] + # ASSERT + self.assertEqual( + expected_dates, + items_created.mapped("date_start") + items_created.mapped("date_end"), + "The wizard should create one pricelist items with" + " the given service (product_id) and dates of sale.", + ) + + def test_several_properties_pricelist(self): + """ + If several properties are set, the wizard should create as + many items as properties. + """ + # ARRANGE + service_spa = self.env["product.product"].create({"name": "Test Spa"}) + pms_property2 = self.env["pms.property"].create( + { + "name": "MY 2nd PMS TEST", + "company_id": self.env.ref("base.main_company").id, + } + ) + date_from = fields.date.today() + date_to = fields.date.today() + expected_properties = [ + self.pms_property1.id, + pms_property2.id, + ] + vals_wizard = { + "massive_changes_on": "pricelist", + "pricelist_ids": [(6, 0, [self.pricelist1.id])], + "apply_pricelists_on": "service", + "service": service_spa.id, + "pms_property_ids": [(6, 0, [self.pms_property1.id, pms_property2.id])], + "start_date": date_from, + "end_date": date_to, + "date_types": "sale_dates", + } + # ACT + self.env["pms.massive.changes.wizard"].create( + vals_wizard + ).apply_massive_changes() + # ASSERT + self.assertEqual( + set(expected_properties), + set( + self.env["product.pricelist.item"] + .search([("pricelist_id", "=", self.pricelist1.id)]) + .mapped("pms_property_ids") + .ids + ), + "The wizard should create as many items as properties given.", + ) diff --git a/pms/tests/test_pms_wizard_split_join_swap_reservation.py b/pms/tests/test_pms_wizard_split_join_swap_reservation.py new file mode 100644 index 0000000000..eae35ebe2a --- /dev/null +++ b/pms/tests/test_pms_wizard_split_join_swap_reservation.py @@ -0,0 +1,1020 @@ +import datetime + +from odoo.exceptions import UserError + +from .common import TestPms + + +class TestPmsWizardSplitJoinSwapReservation(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + # pms.availability.plan + cls.test_availability_plan = cls.env["pms.availability.plan"].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [cls.pricelist1.id])], + } + ) + + # pms.room.type + cls.test_room_type_single = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Single Test", + "default_code": "SNG_Test", + "class_id": cls.room_type_class1.id, + } + ) + # pms.room.type + cls.test_room_type_double = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Double Test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + } + ) + + # create rooms + cls.room1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 101", + "room_type_id": cls.test_room_type_double.id, + "capacity": 2, + } + ) + + cls.room2 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Double 102", + "room_type_id": cls.test_room_type_double.id, + "capacity": 2, + } + ) + + cls.partner1 = cls.env["res.partner"].create({"name": "Antón"}) + + # create a sale channel + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + + # UNIFY TESTS # review + def test_unify_reservation_avail_should(self): + """ + Check that, if there is availability, a reservation with several + rooms on different days can be unified into a one room reservation. + ------------ + Create a reservation with room1.Then, in the first reservation line, + the room is changed to room2.The reservation_join() method of the wizard + is launched, passing the reservation and room2 as parameters and it is + verified that room2 is found in all the reservation lines. + + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 | | r1 | | | | + | Double 102 | | r1 | | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + r1.reservation_line_ids[0].room_id = self.room2 + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservation_join( + r1, self.room2 + ) + # ASSERT + self.assertEqual( + r1.reservation_line_ids.mapped("room_id"), + self.room2, + "The unify operation should assign the indicated room to all nights", + ) + + def test_unify_reservation_avail_not(self): + """ + Check that you cannot unify a reservation with two different rooms + because there is no availability in the required room. + ---------- + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 | r1 | r2 | | | | + | Double 102 | r0 | r0 | r1 | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=2), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.test_room_type_double.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2.flush() + # ACT & ASSERT + with self.assertRaises(UserError): + self.env["pms.reservation.split.join.swap.wizard"].reservation_join( + r1, self.room1 + ) + + def test_unify_reservation_avail_not_room_exist(self): + """ + Check that you cannot unify a reservation with two different rooms + because there the required room does not exists. + """ + + # ARRANGE + + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2.flush() + with self.assertRaises(UserError): + self.env["pms.reservation.split.join.swap.wizard"].reservation_join( + r2, self.env["pms.room"] + ) + + # SWAP TESTS + def test_swap_reservation_rooms_01(self): + # TEST CASE + """ + Check that the rooms of two different reservations was swapped correctly + by applying the reservations_swap() method of the wizard. + ------------ + Initial state + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 | r1 | r1 | | | | + | Double 102 | r2 | r2 | r2 | | | | + +------------+------+------+------+----+----+----+ + + State after swap + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r2 | r2 | r2 | | | | + | Double 102 | r1 | r1 | r1 | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + r2.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservations_swap( + datetime.datetime.now(), + datetime.datetime.now() + datetime.timedelta(days=3), + self.room1.id, + self.room2.id, + ) + # ASSERT + self.assertTrue( + r1.reservation_line_ids.room_id == self.room2 + and r2.reservation_line_ids.room_id == self.room1 + ) + + def test_swap_reservation_rooms_02(self): + """ + Check that two rooms from two different reservations are swapped + correctly. + ------------------- + + Initial state + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | | r1 | r1 | | | | + | Double 102 | r2 | r2 | r2 | | | | + +------------+------+------+------+----+----+----+ + + State after swap + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r2 | r2 | r2 | | | | + | Double 102 | | r1 | r1 | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + r2.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservations_swap( + datetime.datetime.now(), + datetime.datetime.now() + datetime.timedelta(days=3), + self.room1.id, + self.room2.id, + ) + # ASSERT + self.assertTrue( + r1.reservation_line_ids.room_id == self.room2 + and r2.reservation_line_ids.room_id == self.room1 + ) + + def test_swap_reservation_rooms_03(self): + """ + Check that two rooms from two different reservations are swapped + correctly. + ------------------- + Initial state + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | | r1 | r1 | | | | + | Double 102 | r2 | r2 | r2 | | | | + +------------+------+------+------+----+----+----+ + + State after swap + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r2 | r2 | r2 | | | | + | Double 102 | | r1 | r1 | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=1), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + r2.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservations_swap( + datetime.datetime.now(), + datetime.datetime.now() + datetime.timedelta(days=3), + self.room2.id, + self.room1.id, + ) + # ASSERT + self.assertTrue( + r1.reservation_line_ids.room_id == self.room2 + and r2.reservation_line_ids.room_id == self.room1 + ) + + def test_swap_reservation_rooms_04(self): + # TEST CASE + """ + Check that two rooms from two different reservations are swapped + correctly. + source: r1 + target: r2 + -------- + + Initial state + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 | r1 | | | | | + | Double 102 | r2 | r2 | r2 | | | | + +------------+------+------+------+----+----+----+ + + State after swap + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r2 | r2 | | | | | + | Double 102 | r1 | r1 | r2 | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + r2.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservations_swap( + datetime.datetime.now(), + datetime.datetime.now() + datetime.timedelta(days=3), + self.room1.id, + self.room2.id, + ) + # ASSERT + self.assertTrue( + r1.reservation_line_ids.room_id == self.room2 + and r2.reservation_line_ids.room_id == self.room1 + ) + + def test_swap_reservation_rooms_05(self): + """ + Check that two rooms from two different reservations are swapped + correctly. + source: r2 + target: r1 + --------------- + + Initial state + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 | r1 | | | | | + | Double 102 | r2 | r2 | r2 | | | | + +------------+------+------+------+----+----+----+ + + State after swap + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r2 | r2 | r2 | | | | + | Double 102 | r1 | r1 | | | | | + +------------+------+------+------+----+----+----+ + + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + r2.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservations_swap( + datetime.datetime.now(), + datetime.datetime.now() + datetime.timedelta(days=3), + self.room2.id, + self.room1.id, + ) + # ASSERT + self.assertTrue( + r1.reservation_line_ids.room_id == self.room2 + and r2.reservation_line_ids.room_id == self.room1 + ) + + def test_swap_reservation_rooms_06(self): + """ + Check that the room is exchanged correctly for every day because there + is no reservation for another room in those days. + --------------------------- + + Initial state + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | | | | | | | + | Double 102 | r1 | r1 | r1 | | | | + +------------+------+------+------+----+----+----+ + + State after swap + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 | r1 | r1 | | | | + | Double 102 | | | | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservations_swap( + datetime.datetime.now(), + datetime.datetime.now() + datetime.timedelta(days=3), + self.room2.id, + self.room1.id, + ) + # ASSERT + self.assertTrue(r1.reservation_line_ids.room_id == self.room1) + + def test_swap_reservation_rooms_gap_01(self): + """ + Check that three rooms from three different reservations are swapped + correctly. + ----------- + + Initial state + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r0 | | r1 | | | | + | Double 102 | r2 | r2 | r2 | | | | + +------------+------+------+------+----+----+----+ + + State after swap + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r2 | r2 | r2 | | | | + | Double 102 | r0 | | r1 | | | | + +------------+------+------+------+----+----+----+ + """ + + # ARRANGE + + r0 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=2), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + r2.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservations_swap( + datetime.datetime.now(), + datetime.datetime.now() + datetime.timedelta(days=3), + self.room1.id, + self.room2.id, + ) + # ASSERT + self.assertTrue( + r0.reservation_line_ids.room_id == self.room2 + and r1.reservation_line_ids.room_id == self.room2 + and r2.reservation_line_ids.room_id == self.room1 + ) + + def test_swap_reservation_rooms_gap_02(self): + # TEST CASE + """ + Check that three rooms from three different reservations are swapped + correctly. + ----------- + + Initial state + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r0 | | r1 | | | | + | Double 102 | r2 | r2 | r2 | | | | + +------------+------+------+------+----+----+----+ + + State after swap + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r2 | r2 | r2 | | | | + | Double 102 | r0 | | r1 | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r0 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now() + datetime.timedelta(days=2), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r2 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + r2.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservations_swap( + datetime.datetime.now(), + datetime.datetime.now() + datetime.timedelta(days=3), + self.room2.id, + self.room1.id, + ) + # ASSERT + self.assertTrue( + r0.reservation_line_ids.room_id == self.room2 + and r1.reservation_line_ids.room_id == self.room2 + and r2.reservation_line_ids.room_id == self.room1 + ) + + # NOT VALID TEST CASES + def test_swap_reservation_not_valid_01(self): + """ + Check that an error is thrown if you try to pass a room that is + not reserved for those days to the reservations_swap() method. + --------------------------- + Swap room1 with room2 should raise an error because room1 has + no reservation between checkin & checkout provided. + + Initial state + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | | | | | | | + | Double 102 | r1 | r1 | r1 | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT & ACT + with self.assertRaises(UserError): + self.env["pms.reservation.split.join.swap.wizard"].reservations_swap( + datetime.datetime.now(), + datetime.datetime.now() + datetime.timedelta(days=3), + self.room1.id, + self.room2.id, + ) + + # SPLIT TESTS + def test_split_reservation_check_room_splitted_valid_01(self): + """ + A reservation is created with preferred room. The room for 1st night + is switched to another room. + ------------------- + + Expected result: + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | | r1 | r1 | | | | + | Double 102 | r1 | | | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservation_split( + r1, datetime.date.today(), self.room2 + ) + # ASSERT + self.assertTrue( + r1.reservation_line_ids[0].room_id == self.room2 + and r1.reservation_line_ids[1:].room_id == self.room1 + ) + + def test_split_reservation_check_room_splitted_valid_02(self): + """ + A reservation is created with preferred room. The room for 1st + night is switched to another room + -------------- + + Expected result: + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 | r1 | | | | | + | Double 102 | | | r1 | | | | + +------------+------+------+------+----+----+----+ + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservation_split( + r1, + ( + datetime.datetime( + year=datetime.date.today().year, + month=datetime.date.today().month, + day=datetime.date.today().day, + ) + + datetime.timedelta(days=2) + ).date(), + self.room2, + ) + # ASSERT + self.assertTrue( + r1.reservation_line_ids[2].room_id == self.room2 + and r1.reservation_line_ids[:1].room_id == self.room1 + ) + + def test_split_reservation_check_room_splitted_valid_03(self): + """ + A reservation is created with preferred room. The room for 1st + night is switched to another room. + ----------- + + Expected result: + +------------+------+------+------+----+----+----+ + | room/date | 01 | 02 | 03 | 04 | 05 | 06 | + +------------+------+------+------+----+----+----+ + | Double 101 | r1 | | r1 | | | | + | Double 102 | | r1 | | | | | + +------------+------+------+------+----+----+----+""" + + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + # ACT + self.env["pms.reservation.split.join.swap.wizard"].reservation_split( + r1, + ( + datetime.datetime( + year=datetime.date.today().year, + month=datetime.date.today().month, + day=datetime.date.today().day, + ) + + datetime.timedelta(days=1) + ).date(), + self.room2, + ) + # ASSERT + self.assertTrue( + r1.reservation_line_ids[1].room_id == self.room2 + and r1.reservation_line_ids[0].room_id == self.room1 + and r1.reservation_line_ids[2].room_id == self.room1 + ) + + def test_split_reservation_check_room_splitted_not_valid_01(self): + """ + Try to split the reservation for one night and set with a non valid room. + ---------- + Create a reservation for room1. Then create a room and it is deleted. The + reservation_split method is launched but an error should appear because + the room does not exist. + """ + + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + room_not_exist = self.room3 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "Double 103", + "room_type_id": self.test_room_type_double.id, + "capacity": 2, + } + ) + room_not_exist.unlink() + # ACT & ASSERT + with self.assertRaises(UserError): + self.env["pms.reservation.split.join.swap.wizard"].reservation_split( + r1, datetime.datetime.now(), room_not_exist + ) + + def test_split_reservation_check_room_splitted_not_valid_02(self): + # TEST CASE + """ + Try to split the reservation for one night and that night + doesn't belongto reservation. + --------------- + A reservation is created with a date interval of 3 days. + After the reservation_split() method is launched, passing + that reservation but with a date interval of 100 days, + this should throw an error. + """ + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + # ACT & ASSERT + with self.assertRaises(UserError): + self.env["pms.reservation.split.join.swap.wizard"].reservation_split( + r1, datetime.datetime.now() + datetime.timedelta(days=100), self.room1 + ) + + def test_split_reservation_check_room_splitted_not_valid_03(self): + + """ + Try to split the reservation for one night and the reservation + not exists. + ------------- + A reservation is created, but it is not the reservation that is + passed to the reservation_split() method, one that does not exist + is passed to it, this should throw an error. + """ + + # ARRANGE + + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + # ACT & ASSERT + with self.assertRaises(UserError): + self.env["pms.reservation.split.join.swap.wizard"].reservation_split( + self.env["pms.reservation"], datetime.datetime.now(), self.room2 + ) + + def test_split_reservation_check_room_splitted_not_valid_04(self): + """ + Try to split the reservation to one room and the room is not available. + --------------- + A reservation is created with room2 as favorite_room. Another reservation + is created for the same days with room1. An attempt is made to separate + the room from the second reservation using the reservations_split() method, + passing it the same days as the reservations and room2, but this should + throw an error because room2 is not available for those days. + """ + # ARRANGE + + self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room2.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property1.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.flush() + # ACT & ASSERT + with self.assertRaises(UserError): + self.env["pms.reservation.split.join.swap.wizard"].reservation_split( + r1, datetime.datetime.now(), self.room2 + ) diff --git a/pms/tests/test_product_template.py b/pms/tests/test_product_template.py new file mode 100644 index 0000000000..c9a1d97831 --- /dev/null +++ b/pms/tests/test_product_template.py @@ -0,0 +1,327 @@ +import datetime + +from odoo import fields +from odoo.exceptions import ValidationError + +from .common import TestPms + + +class TestProductTemplate(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.room_type = cls.env["pms.room.type"].create( + { + "name": "Room type test", + "default_code": "DBL_Test", + "class_id": cls.room_type_class1.id, + } + ) + cls.room = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Room test", + "room_type_id": cls.room_type.id, + "capacity": 2, + } + ) + cls.partner = cls.env["res.partner"].create({"name": "partner1"}) + cls.board_service = cls.env["pms.board.service"].create( + { + "name": "Board service test", + "default_code": "BST", + } + ) + # create a sale channel + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + + def test_bs_consumed_on_after(self): + """ + Create a one day reservation with a board service configured to + consume after reservation night. + Date of service line with consumed on 'after' should match checkout date. + """ + # ARRANGE + product = self.env["product.product"].create( + { + "name": "Product test", + "per_day": True, + "consumed_on": "after", + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": product.id, + "pms_board_service_id": self.board_service.id, + "adults": True, + } + ) + board_service_room_type = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type.id, + "pms_board_service_id": self.board_service.id, + } + ) + date_checkin = fields.date.today() + date_checkout = fields.date.today() + datetime.timedelta(days=1) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": date_checkin, + "checkout": date_checkout, + "room_type_id": self.room_type.id, + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner.id, + "board_service_room_id": board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + "adults": 2, + } + ) + # ASSERT + self.assertEqual( + reservation.service_ids.service_line_ids.date, + date_checkout, + "Date of service line with consumed on 'after' should match checkout date.", + ) + + def test_bs_consumed_on_before(self): + """ + Create a one day reservation with a board service configured to + consume before reservation night. + Date of service line with consumed on 'before' should match checkin date. + """ + # ARRANGE + product = self.env["product.product"].create( + { + "name": "Product test", + "per_day": True, + "consumed_on": "before", + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": product.id, + "pms_board_service_id": self.board_service.id, + "adults": True, + } + ) + board_service_room_type = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type.id, + "pms_board_service_id": self.board_service.id, + } + ) + date_checkin = fields.date.today() + date_checkout = fields.date.today() + datetime.timedelta(days=1) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": date_checkin, + "checkout": date_checkout, + "room_type_id": self.room_type.id, + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner.id, + "board_service_room_id": board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + "adults": 2, + } + ) + # ASSERT + self.assertEqual( + reservation.service_ids.service_line_ids.date, + date_checkin, + "Date of service line with consumed on 'before' should match checkin date.", + ) + + def test_bs_daily_limit_equal(self): + """ + Create a one day reservation with a board service configured with + daily limit = 2 and capacity = 2 + Reservation should created succesfully. + """ + # ARRANGE + product = self.env["product.product"].create( + { + "name": "Product test", + "per_day": True, + "daily_limit": 2, + "per_person": True, + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": product.id, + "pms_board_service_id": self.board_service.id, + "adults": True, + } + ) + board_service_room_type = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type.id, + "pms_board_service_id": self.board_service.id, + } + ) + date_checkin = fields.date.today() + date_checkout = fields.date.today() + datetime.timedelta(days=1) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": date_checkin, + "checkout": date_checkout, + "room_type_id": self.room_type.id, + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner.id, + "board_service_room_id": board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + reservation.flush() + # ASSERT + self.assertEqual( + reservation.service_ids.service_line_ids.day_qty, + self.room.capacity, + "The reservation should have been created.", + ) + + def test_bs_daily_limit_lower(self): + """ + Create a one day reservation with a board service configured with + daily limit = 2 and capacity = 1 + Reservation should created succesfully. + """ + # ARRANGE + self.room.capacity = 1 + product = self.env["product.product"].create( + { + "name": "Product test", + "per_day": True, + "daily_limit": 2, + "per_person": True, + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": product.id, + "pms_board_service_id": self.board_service.id, + "adults": True, + } + ) + board_service_room_type = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type.id, + "pms_board_service_id": self.board_service.id, + } + ) + date_checkin = fields.date.today() + date_checkout = fields.date.today() + datetime.timedelta(days=1) + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": date_checkin, + "checkout": date_checkout, + "room_type_id": self.room_type.id, + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner.id, + "board_service_room_id": board_service_room_type.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + reservation.flush() + # ASSERT + # self.assertTrue(reservation, "The reservation should have been created.") + # ASSERT + self.assertEqual( + reservation.service_ids.service_line_ids.day_qty, + self.room.capacity, + "The reservation should have been created.", + ) + + def test_bs_daily_limit_greater(self): + """ + Create a one day reservation with a board service configured with + daily limit = 1 and capacity = 2 + Reservation creation should fail. + """ + # ARRANGE + product = self.env["product.product"].create( + { + "name": "Product test", + "per_day": True, + "type": "service", + "daily_limit": 1, + "list_price": 15.0, + "per_person": True, + } + ) + self.env["pms.board.service.line"].create( + { + "product_id": product.id, + "pms_board_service_id": self.board_service.id, + "adults": True, + } + ) + board_service_room_type = self.env["pms.board.service.room.type"].create( + { + "pms_room_type_id": self.room_type.id, + "pms_board_service_id": self.board_service.id, + } + ) + date_checkin = fields.date.today() + date_checkout = fields.date.today() + datetime.timedelta(days=1) + # ACT & ASSERT + with self.assertRaises( + ValidationError, msg="Reservation created but it shouldn't" + ): + self.env["pms.reservation"].create( + { + "checkin": date_checkin, + "checkout": date_checkout, + "room_type_id": self.room_type.id, + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner.id, + "board_service_room_id": board_service_room_type.id, + "adults": 2, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # TODO: Review this test + def _test_bs_is_extra_bed(self): + # ARRANGE + product = self.env["product.product"].create( + { + "name": "Product test", + "per_day": True, + "consumed_on": "after", + "is_extra_bed": True, + } + ) + self.room.capacity = 1 + extra_bed_service = self.env["pms.service"].create( + { + "is_board_service": False, + "product_id": product.id, + } + ) + self.room.extra_beds_allowed = 1 + # ACT + reservation = self.env["pms.reservation"].create( + { + "checkin": fields.date.today(), + "checkout": fields.date.today() + datetime.timedelta(days=1), + "room_type_id": self.room_type.id, + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner.id, + "service_ids": [extra_bed_service.id], + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + reservation._check_adults() + reservation.flush() + + # TODO: pending tests (need review) -> per_day, per_person (with board service?) diff --git a/pms/tests/test_shared_room.py b/pms/tests/test_shared_room.py new file mode 100644 index 0000000000..243990ce8b --- /dev/null +++ b/pms/tests/test_shared_room.py @@ -0,0 +1,509 @@ +import datetime + +from odoo import fields +from odoo.exceptions import ValidationError + +from .common import TestPms + + +class TestPmsSharedRoom(TestPms): + @classmethod + def setUpClass(cls): + super().setUpClass() + # create a room type availability + cls.room_type_availability = cls.env["pms.availability.plan"].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [cls.pricelist1.id])], + } + ) + + cls.bed_class = cls.env["pms.room.type.class"].create( + { + "name": "Bed Class 1", + "default_code": "B1", + } + ) + + # create room type + cls.room_type_test = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Shared Test", + "default_code": "SHT", + "class_id": cls.room_type_class1.id, + } + ) + + cls.room_type_bed = cls.env["pms.room.type"].create( + { + "pms_property_ids": [cls.pms_property1.id], + "name": "Bed Type Test", + "default_code": "BTT", + "class_id": cls.bed_class.id, + } + ) + + # create shared room + cls.room1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "Shared 101", + "room_type_id": cls.room_type_test.id, + "capacity": 2, + } + ) + + # create beds in room1 + cls.r1bed1 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "101 (1)", + "room_type_id": cls.room_type_bed.id, + "capacity": 1, + "parent_id": cls.room1.id, + } + ) + + cls.r1bed2 = cls.env["pms.room"].create( + { + "pms_property_id": cls.pms_property1.id, + "name": "101 (2)", + "room_type_id": cls.room_type_bed.id, + "capacity": 2, + "parent_id": cls.room1.id, + } + ) + + # create partner + cls.partner1 = cls.env["res.partner"].create( + { + "firstname": "Jaime", + "lastname": "García", + "email": "jaime@example.com", + "birthdate_date": "1983-03-01", + "gender": "male", + } + ) + + # create a sale channel + cls.sale_channel_direct1 = cls.env["pms.sale.channel"].create( + { + "name": "Door", + "channel_type": "direct", + } + ) + + def test_count_avail_beds_with_room_occupied(self): + """ + Check that not allow to create a bed reservation with a room occupied + ---------------- + Create a room1 reservation and check that the beds room real avail is 0 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.room1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT + self.assertEqual( + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).availability, + 0, + "Beds avaialbility should be 0 for room occupied", + ) + + def test_count_avail_shared_room_with_one_bed_occupied(self): + """ + Check that not allow to create a shared room reservation with a bed occupied + ---------------- + Create a room1's bed reservation and check that the room1 real avail is 0 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT + self.assertEqual( + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_test.id, + ).availability, + 0, + "Shared Room avaialbility should be 0 if it has a bed occupied", + ) + + def test_avail_in_room_type_with_shared_rooms(self): + """ + Check that a shared room's bed occupied not + affect the avail on other rooms with the + same room type + ---------------- + Create other room like room_type_test (room2) + Create a room1's bed reservation and check that the room1 + Check that room_type_test real avail is 1 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + self.room2 = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "Shared 102", + "room_type_id": self.room_type_test.id, + "capacity": 2, + } + ) + + # ACT + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT + self.assertEqual( + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_test.id, + ).availability, + 1, + "Room not shared affect by the shared room's avail with the same type", + ) + + def test_count_avail_beds_with_one_bed_occupied(self): + """ + Check the avail of a bed when it has + a room with other beds occupied + ---------------- + Create a room1's bed (it has 2 beds) + reservation and check that the beds avail = 1 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + res1 = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + res1.flush() + # ASSERT + self.assertEqual( + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).availability, + 1, + "Beds avaialbility should be 1 if it has 1 of 2 beds occupied", + ) + + def test_not_avail_beds_with_room_occupied(self): + """ + Check that not allow to select a bed with a room occupied + ---------------- + Create a room1 reservation and check that the beds are not available + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.room1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT + self.assertNotIn( + self.r1bed1.id, + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).free_room_ids.ids, + "room's bed should not be available " "because the entire room is reserved", + ) + + def test_not_avail_shared_room_with_one_bed_occupied(self): + """ + Check that not allow to select a shared + room with a bed occupied + ---------------- + Create a room1's bed reservation and check + that the room1 real avail is not available + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT + self.assertNotIn( + self.room1.id, + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).free_room_ids.ids, + "Entire Shared room should not be available " + "becouse it has a bed occupied", + ) + + def test_avail_beds_with_one_bed_occupied(self): + """ + Check the select of a bed when it has a + room with other beds occupied + ---------------- + Create a room1's bed (it has 2 beds) reservation + and check that the other bed is avail + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ASSERT + self.assertIn( + self.r1bed2.id, + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).free_room_ids.ids, + "The bed2 of the shared room should be available", + ) + + def test_not_allowed_reservation_in_bed_with_room_occuppied(self): + """ + Check the constrain that not allow to create a reservation in a bed in a + room with other reservation like shared + ---------------- + Create a room1's reservation and the try to create a reservation + in the room1's bed, we expect an error + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.room1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Reservation created on a bed whose room was already occupied", + ): + r_test = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.flush() + + def test_not_allowed_reservation_in_shared_room_with_bed_occuppied(self): + """ + Check the constrain that not allow to create a reservation + in a shared room in a bed reservation + ---------------- + Create a room1's bed reservation and the try to create + a reservation in the room1, we expect an error + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + + # ACT & ASSERT + with self.assertRaises( + ValidationError, + msg="Reservation created in a full shared " + "room that already had beds occupied", + ): + r_test = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.room1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r_test.flush() + + def check_room_shared_availability_released_when_canceling_bed_reservations(self): + """ + Check that check availability in shared room is + released when canceling bed reservations + ---------------- + Create a room1's bed reservation and then cancel it, + check that the room1 real avail is 1 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + r1 = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.r1bed1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.action_cancel() + + # ASSERT + self.assertEqual( + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_test.id, + ).availability, + 1, + "The parent room avail dont update " "when cancel child room reservation", + ) + + def check_bed_availability_released_when_canceling_parent_room_reservations(self): + """ + Check that check availability in child room is + released when canceling the parent rooms + ---------------- + Create a room1 reservation and then cancel it, + check that the beds real avail is 2 + """ + + # ARRANGE + today = fields.date.today() + tomorrow = fields.date.today() + datetime.timedelta(days=1) + + # ACT + r1 = self.env["pms.reservation"].create( + { + "partner_id": self.partner1.id, + "preferred_room_id": self.room1.id, + "checkin": today, + "checkout": tomorrow, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct1.id, + } + ) + r1.action_cancel() + + # ASSERT + self.assertEqual( + self.pms_property1.with_context( + checkin=today, + checkout=tomorrow, + room_type_id=self.room_type_bed.id, + ).availability, + 2, + "The child room avail dont update when " "cancel parent room reservation", + ) diff --git a/pms/tests/test_tourist_taxes.py b/pms/tests/test_tourist_taxes.py new file mode 100644 index 0000000000..3abd2361c1 --- /dev/null +++ b/pms/tests/test_tourist_taxes.py @@ -0,0 +1,143 @@ +import datetime + +from odoo import fields +from odoo.tests.common import TransactionCase + + +class TestTouristTaxes(TransactionCase): + def setUp(self): + super(TestTouristTaxes, self).setUp() + self.product_tourist_tax = self.env["product.product"].create( + { + "name": "Tourist Tax", + "is_tourist_tax": True, + "touristic_calculation": "occupancy", + "occupancy_domain": "[('state', '!=', 'cancel')]", + "nights_domain": "[('state', '!=', 'cancel')]", + } + ) + self.partner = self.env["res.partner"].create( + { + "name": "Test Partner", + } + ) + self.room_type = self.env["pms.room.type"].create( + { + "name": "Test Room Type", + "product_id": self.env["product.product"] + .create( + { + "name": "Room Product", + "type": "service", + } + ) + .id, + } + ) + self.room = self.env["pms.room"].create( + { + "name": "Test Room", + "room_type_id": self.room_type.id, + } + ) + self.reservation = self.env["pms.reservation"].create( + { + "partner_id": self.partner.id, + "room_type_id": self.room_type.id, + "checkin": fields.Date.today(), + "checkout": fields.Date.today() + datetime.timedelta(days=2), + "adults": 2, + } + ) + + def test_add_tourist_tax_service(self): + """ + Test that a tourist tax service is created when adding a reservation. + Steps: + 1. Add a tourist tax service to the reservation. + 2. Search for the created service. + 3. Assert that the service is created and the quantity is correct. + """ + self.reservation._add_tourist_tax_service() + service = self.env["pms.service"].search( + [ + ("reservation_id", "=", self.reservation.id), + ("product_id", "=", self.product_tourist_tax.id), + ] + ) + self.assertEqual(len(service), 1, "Tourist tax service should be created") + self.assertEqual(service.quantity, 2, "Tourist tax quantity should be 2") + + def test_update_tourist_tax_service(self): + """ + Test that a tourist tax service is updated when modifying the reservation. + Steps: + 1. Add a tourist tax service to the reservation. + 2. Update the number of adults in the reservation. + 3. Update the tourist tax service. + 4. Search for the updated service. + 5. Assert that the service is updated and the quantity is correct. + """ + self.reservation._add_tourist_tax_service() + self.reservation.adults = 3 + self.reservation._update_tourist_tax_service() + service = self.env["pms.service"].search( + [ + ("reservation_id", "=", self.reservation.id), + ("product_id", "=", self.product_tourist_tax.id), + ] + ) + self.assertEqual(len(service), 1, "Tourist tax service should be updated") + self.assertEqual( + service.quantity, 3, "Tourist tax quantity should be updated to 3" + ) + + def test_no_tourist_tax_service_when_quantity_zero(self): + """ + Test that no tourist tax service is created when the quantity is zero. + Steps: + 1. Set the tourist tax calculation to 'occupancyandnights'. + 2. Add a tourist tax service to the reservation. + 3. Search for the created service. + 4. Assert that no service is created. + """ + self.product_tourist_tax.touristic_calculation = "occupancyandnights" + self.reservation._add_tourist_tax_service() + service = self.env["pms.service"].search( + [ + ("reservation_id", "=", self.reservation.id), + ("product_id", "=", self.product_tourist_tax.id), + ] + ) + self.assertEqual( + len(service), + 0, + "Tourist tax service should not be created when quantity is zero", + ) + + def test_remove_tourist_tax_service_when_quantity_zero(self): + """ + Test that a tourist tax service is removed when the quantity becomes zero. + Steps: + 1. Set the tourist tax calculation to 'occupancy'. + 2. Add a tourist tax service to the reservation. + 3. Update the number of adults in the reservation to zero. + 4. Update the tourist tax service. + 5. Search for the updated service. + 6. Assert that the service is removed. + """ + self.product_tourist_tax.touristic_calculation = "occupancy" + self.reservation._add_tourist_tax_service() + self.reservation.adults = 0 + self.reservation._update_tourist_tax_service() + service = self.env["pms.service"].search( + [ + ("reservation_id", "=", self.reservation.id), + ("product_id", "=", self.product_tourist_tax.id), + ] + ) + self.assertEqual( + len(service), + 0, + "Tourist tax service should be removed when quantity is zero", + ) diff --git a/pms/views/account_analytic_distribution_views.xml b/pms/views/account_analytic_distribution_views.xml new file mode 100644 index 0000000000..cfd0768567 --- /dev/null +++ b/pms/views/account_analytic_distribution_views.xml @@ -0,0 +1,26 @@ + + + + + account.analytic.distribution.model.inherit.form + account.analytic.distribution.model + + + + + + + + + + account.analytic.distribution.model.inherit.tree + account.analytic.distribution.model + + + + + + + + + diff --git a/pms/views/account_analytic_line_views.xml b/pms/views/account_analytic_line_views.xml new file mode 100644 index 0000000000..9dfb879dbf --- /dev/null +++ b/pms/views/account_analytic_line_views.xml @@ -0,0 +1,18 @@ + + + + + account.analytic.line.tree.inherit.account + account.analytic.line + + + + + + + + + diff --git a/pms/views/account_bank_statement_views.xml b/pms/views/account_bank_statement_views.xml new file mode 100644 index 0000000000..31e0983843 --- /dev/null +++ b/pms/views/account_bank_statement_views.xml @@ -0,0 +1,13 @@ + + + + account.bank.statement + + + + + + + + + diff --git a/pms/views/account_journal_views.xml b/pms/views/account_journal_views.xml new file mode 100644 index 0000000000..de104fd4d1 --- /dev/null +++ b/pms/views/account_journal_views.xml @@ -0,0 +1,54 @@ + + + + account.journal + + + + + + + + + + + + + + account.journal + + + + + + + + + + account.journal + + + + + + + + diff --git a/pms/views/account_move_line_views.xml b/pms/views/account_move_line_views.xml new file mode 100644 index 0000000000..a272e096a1 --- /dev/null +++ b/pms/views/account_move_line_views.xml @@ -0,0 +1,31 @@ + + + + + account.move.line + + + + + + + + + + account.move.line + + + + + + + + + + + + diff --git a/pms/views/account_move_views.xml b/pms/views/account_move_views.xml new file mode 100644 index 0000000000..acfa6e4445 --- /dev/null +++ b/pms/views/account_move_views.xml @@ -0,0 +1,125 @@ + + + + account.move + + + + + + + + + + + + + + + + + + + + + + account.move + + + + + + + + + + + account.move + + + + + + + + + + + + + + account.move + + + + + + + + + + + + + + PMS Invoices + account.move + tree,kanban,form + + + [('move_type', 'in', ('out_invoice','out_refund'))] + {'default_move_type': 'out_invoice'} + +

+ Create a customer invoice +

+ Create invoices, register payments and keep track of the discussions with your customers. +

+
+
+ + +
diff --git a/pms/views/account_payment_views.xml b/pms/views/account_payment_views.xml new file mode 100644 index 0000000000..790b01130d --- /dev/null +++ b/pms/views/account_payment_views.xml @@ -0,0 +1,62 @@ + + + + account.payment + + + + + + + + + + + + + + + diff --git a/pms/views/account_portal_templates.xml b/pms/views/account_portal_templates.xml new file mode 100644 index 0000000000..3315c652d5 --- /dev/null +++ b/pms/views/account_portal_templates.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/pms/views/assets.xml b/pms/views/assets.xml new file mode 100644 index 0000000000..5f19088260 --- /dev/null +++ b/pms/views/assets.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/pms/views/folio_portal_templates.xml b/pms/views/folio_portal_templates.xml new file mode 100644 index 0000000000..c09bb7f7f7 --- /dev/null +++ b/pms/views/folio_portal_templates.xml @@ -0,0 +1,250 @@ + + + + + + + +