diff --git a/rental_pricelist/README.rst b/rental_pricelist/README.rst new file mode 100644 index 00000000..3307ed1b --- /dev/null +++ b/rental_pricelist/README.rst @@ -0,0 +1,126 @@ +================ +Rental Pricelist +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:abb8d54f6920d71a4ca8c6ffe2552ecebbbca7f055e9029cc8ef05bc6d3779db + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fvertical--rental-lightgray.png?logo=github + :target: https://github.com/OCA/vertical-rental/tree/16.0/rental_pricelist + :alt: OCA/vertical-rental +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/vertical-rental-16-0/vertical-rental-16-0-rental_pricelist + :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/vertical-rental&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +*This file has been generated on 2022-05-04-12-21-41. Changes to it will be overwritten.* + +Enables the user to define different rental prices with time uom (Month, Day and Hour). + +Rental prices are usually scaled prices based on a time unit, typically day, sometimes months or hour. +This modules integrates the standard Odoo pricelists into rental use cases and allows the user an +easy way to specify the prices in a product tab as well as to use all the enhanced pricelist features. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Create a rentable product: + * Go to Rentals > Configuration > Settings. + * Please activate the checkbox for using 'Product Variants'. + * Go to Rentals > Products > Products. + * Create a new storable product. + * Active the checkbox 'Can be Rented'. + + Configure the naming of rental services: + * Go to Settings > Users & Companies > Companies. + * To to page 'Rental Services'. + * Configure the rental service names by providing a prefix and suffix for the name and default code. + + Create the rental services: + * Go to the previously created rentable storable product. + * Go to page 'Rental Price'. + * Activate the boolean fields for hourly, daily or monthly rental as needed. + * Save the product, which creates the related rental services for the given time units. + * Add a usual price for one hour, one day or one month. + * Add bulk prices, e.g. one day costs 300 €, 7 days 290 €, 21 days 250 €, and so on. + +Hint: The (bulk) prices are added in the product form view of the storable, rentable product +but are actually used for its related rental services! + +Create a rental order: + * Go to Rentals > Customer > Rental Quotations. + * Create a new order and choose the type 'Rental Order'. + * Choose the storable rental product (not the rental service!). + * Choose the rental time unit, which actually loads the correct related rental service. + * Set the quantity to rent out one or several storable rentable products. + * Choose start and end date. + * Confirm the order. + * Check out the two deliveries, one for outgoing and one for incoming delivery. + +Please also see the usage section of sale_rental and rental_base module. + +Changelog +========= + +- 8d191ff7 2022-04-10 15:41:16 +0200 wagner@elegosoft.com add missing/lost documentation (issue #4516) +- 4509f78a 2022-02-23 20:48:33 +0100 wagner@elegosoft.com (origin/feature_4516_add_files_ported_from_v12_v14, feature_4516_add_files_ported_from_v12_v14) add files ported to v14 by cpatel and khanhbui (issue #4516) + +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 +~~~~~~~ + +* elego Software Solutions GmbH + +Contributors +~~~~~~~~~~~~ + +elego Software Solutions GmbH, Odoo Community Association (OCA) + +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/vertical-rental `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/rental_pricelist/README/CONFIGURATION.rst b/rental_pricelist/README/CONFIGURATION.rst new file mode 100644 index 00000000..cff41a13 --- /dev/null +++ b/rental_pricelist/README/CONFIGURATION.rst @@ -0,0 +1,8 @@ +To configure this module, you need to: + +#. Go to company settings and define the default interval ranges on 'Rental Interval Prices' tab. + These ranges will be applied for computation of price intervals for rental service products when interval pricing is activated + in stockable product. + +#. If desired go to 'RS (Prefix and Suffix)' tab an define how rental interval service product + names and reference numbers are created. diff --git a/rental_pricelist/README/CONTRIBUTORS.rst b/rental_pricelist/README/CONTRIBUTORS.rst new file mode 100644 index 00000000..980305e1 --- /dev/null +++ b/rental_pricelist/README/CONTRIBUTORS.rst @@ -0,0 +1 @@ +elego Software Solutions GmbH, Odoo Community Association (OCA) diff --git a/rental_pricelist/README/DESCRIPTION.rst b/rental_pricelist/README/DESCRIPTION.rst new file mode 100644 index 00000000..4afffbf0 --- /dev/null +++ b/rental_pricelist/README/DESCRIPTION.rst @@ -0,0 +1,7 @@ +*This file has been generated on 2022-05-04-12-21-41. Changes to it will be overwritten.* + +Enables the user to define different rental prices with time uom (Month, Day and Hour). + +Rental prices are usually scaled prices based on a time unit, typically day, sometimes months or hour. +This modules integrates the standard Odoo pricelists into rental use cases and allows the user an +easy way to specify the prices in a product tab as well as to use all the enhanced pricelist features. diff --git a/rental_pricelist/README/HISTORY.rst b/rental_pricelist/README/HISTORY.rst new file mode 100644 index 00000000..0ffc209c --- /dev/null +++ b/rental_pricelist/README/HISTORY.rst @@ -0,0 +1,2 @@ +- 8d191ff7 2022-04-10 15:41:16 +0200 wagner@elegosoft.com add missing/lost documentation (issue #4516) +- 4509f78a 2022-02-23 20:48:33 +0100 wagner@elegosoft.com (origin/feature_4516_add_files_ported_from_v12_v14, feature_4516_add_files_ported_from_v12_v14) add files ported to v14 by cpatel and khanhbui (issue #4516) diff --git a/rental_pricelist/README/USAGE.rst b/rental_pricelist/README/USAGE.rst new file mode 100644 index 00000000..f4f0519b --- /dev/null +++ b/rental_pricelist/README/USAGE.rst @@ -0,0 +1,34 @@ +Create a rentable product: + * Go to Rentals > Configuration > Settings. + * Please activate the checkbox for using 'Product Variants'. + * Go to Rentals > Products > Products. + * Create a new storable product. + * Active the checkbox 'Can be Rented'. + + Configure the naming of rental services: + * Go to Settings > Users & Companies > Companies. + * To to page 'Rental Services'. + * Configure the rental service names by providing a prefix and suffix for the name and default code. + + Create the rental services: + * Go to the previously created rentable storable product. + * Go to page 'Rental Price'. + * Activate the boolean fields for hourly, daily or monthly rental as needed. + * Save the product, which creates the related rental services for the given time units. + * Add a usual price for one hour, one day or one month. + * Add bulk prices, e.g. one day costs 300 €, 7 days 290 €, 21 days 250 €, and so on. + +Hint: The (bulk) prices are added in the product form view of the storable, rentable product +but are actually used for its related rental services! + +Create a rental order: + * Go to Rentals > Customer > Rental Quotations. + * Create a new order and choose the type 'Rental Order'. + * Choose the storable rental product (not the rental service!). + * Choose the rental time unit, which actually loads the correct related rental service. + * Set the quantity to rent out one or several storable rentable products. + * Choose start and end date. + * Confirm the order. + * Check out the two deliveries, one for outgoing and one for incoming delivery. + +Please also see the usage section of sale_rental and rental_base module. diff --git a/rental_pricelist/__init__.py b/rental_pricelist/__init__.py new file mode 100644 index 00000000..79f89329 --- /dev/null +++ b/rental_pricelist/__init__.py @@ -0,0 +1,4 @@ +# Part of rental-vertical See LICENSE file for full copyright and licensing details. + +from .hooks import set_multi_sales_price +from . import models diff --git a/rental_pricelist/__manifest__.py b/rental_pricelist/__manifest__.py new file mode 100644 index 00000000..4169d88d --- /dev/null +++ b/rental_pricelist/__manifest__.py @@ -0,0 +1,26 @@ +# Part of rental-vertical See LICENSE file for full copyright and licensing details. + +{ + "name": "Rental Pricelist", + "summary": "Enables the user to define different rental prices with " + "time uom (Month, Day and Hour).", + "version": "17.0.1.0.0", + "category": "Rental", + "author": "elego Software Solutions GmbH, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/vertical-rental", + "depends": [ + "rental_base", + ], + "data": [ + "views/sale_view.xml", + "views/product_view.xml", + "views/product_template_view.xml", + "views/res_company_view.xml", + ], + "demo": [], + "qweb": [], + "post_init_hook": "set_multi_sales_price", + "application": False, + "installable": True, + "license": "AGPL-3", +} diff --git a/rental_pricelist/hooks.py b/rental_pricelist/hooks.py new file mode 100644 index 00000000..771d767f --- /dev/null +++ b/rental_pricelist/hooks.py @@ -0,0 +1,9 @@ +# Part of rental-vertical See LICENSE file for full copyright and licensing details. + + +def set_multi_sales_price(env): + conf_page = env["res.config.settings"].create({}) + conf_page.group_uom = True + conf_page.group_product_pricelist = True + conf_page.product_pricelist_setting = "advanced" + conf_page.execute() diff --git a/rental_pricelist/i18n/de.po b/rental_pricelist/i18n/de.po new file mode 100644 index 00000000..3a598700 --- /dev/null +++ b/rental_pricelist/i18n/de.po @@ -0,0 +1,601 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * rental_pricelist +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-17 11:34+0000\n" +"PO-Revision-Date: 2022-01-17 11:34+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: rental_pricelist +#: model_terms:ir.ui.view,arch_db:rental_pricelist.view_order_form +msgid " " +msgstr "" + +#. module: rental_pricelist +#: model_terms:ir.ui.view,arch_db:rental_pricelist.view_company_rental_service_form +msgid "" +"Configure the name and default code of your rental services." +msgstr "" +"Hier können der Name und die interne Referenz der Vermietungsservices " +"konfiguriert werden." + +#. module: rental_pricelist +#: model_terms:ir.ui.view,arch_db:rental_pricelist.product_normal_form_view +msgid "" +" 0 + and line.order_id.type_id.id + == self.env.ref("rental_base.rental_sale_type").id + and line.rental_qty == 0 + ): + line.rental_qty = line.product_uom_qty + return lines + + @api.model + def _get_product_domain(self): + domain = [ + "|", + "&", + ("type", "=", "product"), + "|", + ("sale_ok", "=", True), + ("rental", "=", True), + "&", + ("type", "=", "service"), + "&", + ("sale_ok", "=", True), + ("rental", "=", False), + ] + return domain + + def _set_product_id(self): + self.ensure_one() + if self.rental and self.display_product_id: + time_uoms = self._get_time_uom() + if self.display_product_id.rental_of_day: + self.product_uom = time_uoms["day"] + self.product_id = self.display_product_id.product_rental_day_id + elif self.display_product_id.rental_of_month: + self.product_uom = time_uoms["month"] + self.product_id = self.display_product_id.product_rental_month_id + elif self.display_product_id.rental_of_hour: + self.product_uom = time_uoms["hour"] + self.product_id = self.display_product_id.product_rental_hour_id + else: + self.rental = False + self.product_id = self.display_product_id + elif not self.rental and self.display_product_id: + self.product_id = self.display_product_id + + @api.onchange("display_product_id") + def onchange_display_product_id(self): + if self.display_product_id: + self.product_id = self.display_product_id + if self.display_product_id.rental: + self.rental = True + self.rental = False + self.can_sell_rental = False + rental_type_id = self.env.ref("rental_base.rental_sale_type").id + if self.env.context.get("type_id", False) == rental_type_id: + self.rental = True + self._set_product_id() + + @api.onchange("rental") + def onchange_rental(self): + if self.rental: + self.can_sell_rental = False + self.sell_rental_id = False + rental_type_id = self.env.ref("rental_base.rental_sale_type").id + if self.env.context.get("type_id", False) == rental_type_id: + self.rental_qty = 1 + else: + self.rental_type = False + self.rental_qty = 0 + self.extension_rental_id = False + self._set_product_id() + + @api.onchange("can_sell_rental") + def onchange_can_sell_rental(self): + if self.can_sell_rental: + self.rental = False + self.rental_type = False + self.rental_qty = 0 + self.extension_rental_id = False + self.product_id = self.display_product_id + else: + self.sell_rental_id = False + self._set_product_id() + + def _check_rental_availability(self): + res = {} + self.ensure_one() + product_uom = self.product_id.rented_product_id.uom_id + warehouse = self.order_id.warehouse_id + rental_in_location = warehouse.rental_in_location_id + rented_product_ctx = self.with_context( + location=rental_in_location.id + ).product_id.rented_product_id + in_location_available_qty = ( + rented_product_ctx.qty_available - rented_product_ctx.outgoing_qty + ) + compare_qty = float_compare( + in_location_available_qty, + self.rental_qty, + precision_rounding=product_uom.rounding, + ) + if compare_qty == -1: + rental_uom_name = self.product_id.rented_product_id.uom_id.name + res["warning"] = { + "title": _("Not enough stock!"), + "message": _( + f"You want to rent {self.rental_qty}.2f {rental_uom_name} " + f"but you only " + f"have {in_location_available_qty}.2f " + f"{rental_uom_name} currently available " + "on the " + f'stock location "{rental_in_location.name}"! Make sure that you ' + "get some units back in the meantime or " + f're-supply the stock location "{rental_in_location.name}".' + ), + } + return res + + # Override function in rental_sale + @api.onchange("product_id", "rental_qty") + def rental_product_id_change(self): + res = {} + if self.product_id: + if self.product_id.rented_product_id: + self.sell_rental_id = False + if not self.rental_type: + self.rental_type = "new_rental" + elif ( + self.rental_type == "new_rental" + and self.rental_qty + and self.order_id.warehouse_id + ): + avail = self._check_rental_availability() + if avail.get("warning", False): + res["warning"] = avail["warning"] + elif self.product_id.rental_service_ids: + self.rental_type = False + self.rental_qty = 0 + self.extension_rental_id = False + else: + self.rental_type = False + self.rental_qty = 0 + self.extension_rental_id = False + self.sell_rental_id = False + else: + self.rental_type = False + self.rental_qty = 0 + self.extension_rental_id = False + self.sell_rental_id = False + return res + + @api.constrains( + "rental_type", + "extension_rental_id", + "start_date", + "end_date", + "rental_qty", + "product_uom_qty", + "product_id", + ) + def _check_sale_line_rental(self): + for line in self: + if line.rental_type == "rental_extension": + if not line.extension_rental_id: + raise ValidationError( + _( + 'Missing "Rental to Extend" on the sale order line ' + "with rental service %(name)s." + ) + % {"name": line.product_id.name} + ) + + if line.rental_qty != line.extension_rental_id.rental_qty: + raise ValidationError( + _( + "On the sale order line with rental service %(name)s, " + "you are trying to extend a rental with a rental " + "quantity (%(rental_qty)s) that is different from " + "the quantity of the original rental " + "(%(ext_rental_qty)s). This is not supported." + ) + % { + "name": line.product_id.name, + "rental_qty": line.rental_qty, + "ext_rental_qty": line.extension_rental_id.rental_qty, + } + ) + if line.rental_type in ("new_rental", "rental_extension"): + if not line.product_id.rented_product_id: + raise ValidationError( + _( + 'On the "new rental" sale order line with product ' + '"%(name)s", we should have a rental service product!' + ) + % {"name": line.product_id.name} + ) + elif line.sell_rental_id: + if line.product_uom_qty != line.sell_rental_id.rental_qty: + raise ValidationError( + _( + "On the sale order line with product %(name)s " + "you are trying to sell a rented product with a " + "quantity (%(qty)s) that is different from the rented " + "quantity (%(rental_qty)s). This is not supported." + ) + % { + "name": line.product_id.name, + "qty": line.product_uom_qty, + "rental_qty": line.sell_rental_id.rental_qty, + } + ) + + @api.onchange("rental_qty", "number_of_time_unit", "product_id") + def rental_qty_number_of_days_change(self): + if self.product_id.rented_product_id: + qty = self.rental_qty * self.number_of_time_unit + self.product_uom_qty = qty + + def _get_product_rental_uom_ids(self): + self.ensure_one() + time_uoms = self._get_time_uom() + uom_ids = [] + if self.display_product_id.rental_of_month: + uom_ids.append(time_uoms["month"].id) + if self.display_product_id.rental_of_day: + uom_ids.append(time_uoms["day"].id) + if self.display_product_id.rental_of_hour: + uom_ids.append(time_uoms["hour"].id) + return uom_ids + + @api.onchange("product_id") + def _onchange_product_id(self): + for line in self: + if line.rental: + if line.display_product_id.rental: + uom_ids = line._get_product_rental_uom_ids() + if line.product_uom.id not in uom_ids: + line.product_uom = uom_ids and uom_ids[0] or False + + @api.onchange("product_uom", "product_uom_qty") + def _onchange_product_uom(self): + for line in self: + if line.rental and line.display_product_id: + if line.product_uom.id != line.product_id.uom_id.id: + time_uoms = line._get_time_uom() + key = list( + filter( + lambda key: time_uoms[key].id == line.product_uom.id, + time_uoms.keys(), + ) + )[-1] + line.product_id = line.display_product_id._get_rental_service(key) + + @api.onchange("start_date", "end_date", "product_uom") + def _onchange_start_end_date(self): + if self.start_date and self.end_date: + number = self._get_number_of_time_unit() + self.number_of_time_unit = number diff --git a/rental_pricelist/pyproject.toml b/rental_pricelist/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/rental_pricelist/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/rental_pricelist/static/description/icon.png b/rental_pricelist/static/description/icon.png new file mode 100644 index 00000000..9cd25ce6 Binary files /dev/null and b/rental_pricelist/static/description/icon.png differ diff --git a/rental_pricelist/static/description/index.html b/rental_pricelist/static/description/index.html new file mode 100644 index 00000000..5c00c65f --- /dev/null +++ b/rental_pricelist/static/description/index.html @@ -0,0 +1,428 @@ + + + + + + +Rental Pricelist + + + +
+

Rental Pricelist

+ +

This file has been generated on 2022-05-04-12-21-41. Changes to it will be overwritten.

+
+

Summary

+

Enables the user to define different rental prices with time uom (Month, Day and Hour).

+
+
+

Description

+

Rental prices are usually scaled prices based on a time unit, typically day, sometimes months or hour. +This modules integrates the standard Odoo pricelists into rental use cases and allows the user an +easy way to specify the prices in a product tab as well as to use all the enhanced pricelist features.

+
+
+

Usage

+
+
Create a rentable product:
+
    +
  • Go to Rentals > Configuration > Settings.
  • +
  • Please activate the checkbox for using 'Product Variants'.
  • +
  • Go to Rentals > Products > Products.
  • +
  • Create a new storable product.
  • +
  • Active the checkbox 'Can be Rented'.
  • +
+

Configure the naming of rental services: +* Go to Settings > Users & Companies > Companies. +* To to page 'Rental Services'. +* Configure the rental service names by providing a prefix and suffix for the name and default code.

+

Create the rental services: +* Go to the previously created rentable storable product. +* Go to page 'Rental Price'. +* Activate the boolean fields for hourly, daily or monthly rental as needed. +* Save the product, which creates the related rental services for the given time units. +* Add a usual price for one hour, one day or one month. +* Add bulk prices, e.g. one day costs 300 €, 7 days 290 €, 21 days 250 €, and so on.

+
+
+

Hint: The (bulk) prices are added in the product form view of the storable, rentable product +but are actually used for its related rental services!

+
+
Create a rental order:
+
    +
  • Go to Rentals > Customer > Rental Quotations.
  • +
  • Create a new order and choose the type 'Rental Order'.
  • +
  • Choose the storable rental product (not the rental service!).
  • +
  • Choose the rental time unit, which actually loads the correct related rental service.
  • +
  • Set the quantity to rent out one or several storable rentable products.
  • +
  • Choose start and end date.
  • +
  • Confirm the order.
  • +
  • Check out the two deliveries, one for outgoing and one for incoming delivery.
  • +
+
+
+

Please also see the usage section of sale_rental and rental_base module.

+
+
+

Changelog

+
    +
  • 8d191ff7 2022-04-10 15:41:16 +0200 wagner@elegosoft.com add missing/lost documentation (issue #4516)
  • +
  • 4509f78a 2022-02-23 20:48:33 +0100 wagner@elegosoft.com (origin/feature_4516_add_files_ported_from_v12_v14, feature_4516_add_files_ported_from_v12_v14) add files ported to v14 by cpatel and khanhbui (issue #4516)
  • +
+
+
+ + diff --git a/rental_pricelist/tests/__init__.py b/rental_pricelist/tests/__init__.py new file mode 100644 index 00000000..29bc3cc0 --- /dev/null +++ b/rental_pricelist/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of rental-vertical See LICENSE file for full copyright and licensing details. +from . import test_rental_pricelist diff --git a/rental_pricelist/tests/test_rental_pricelist.py b/rental_pricelist/tests/test_rental_pricelist.py new file mode 100644 index 00000000..79d7717a --- /dev/null +++ b/rental_pricelist/tests/test_rental_pricelist.py @@ -0,0 +1,593 @@ +# Part of rental-vertical See LICENSE file for full copyright and licensing details. + +from dateutil.relativedelta import relativedelta + +from odoo import exceptions, fields +from odoo.exceptions import ValidationError + +from odoo.addons.rental_base.tests.stock_common import RentalStockCommon + + +def _run_sol_onchange_display_product_id(line): + line.onchange_display_product_id() # product_id, rental changed + line._onchange_product_id() + line.onchange_rental() # product_id changed again + line._onchange_product_id() # product_uom changed + line._onchange_product_uom() + line.rental_product_id_change() # set start end date manually + + +def _run_sol_onchange_date(line, start_date=False, end_date=False): + if start_date: + line.start_date = start_date + if end_date: + line.end_date = end_date + line._onchange_start_end_date() + line.rental_qty_number_of_days_change() + line._onchange_product_uom() + + +def _run_sol_onchange_product_uom(line, product_uom): + line.product_uom = product_uom + line._onchange_product_uom() + line._onchange_product_id() + line._onchange_start_end_date() + line.rental_qty_number_of_days_change() + line._onchange_product_uom() + + +def _run_sol_onchange_can_sell_rental(line, can_sell_rental): + line.can_sell_rental = can_sell_rental + line.onchange_can_sell_rental() + line.onchange_rental() + line._onchange_product_id() + line._onchange_product_uom() + + +def _run_sol_onchange_rental(line, rental): + line.rental = rental + line.onchange_rental() + line.onchange_can_sell_rental() + line.rental_product_id_change() + line._onchange_product_id() + line._onchange_product_uom() + + +class TestRentalPricelist(RentalStockCommon): + def setUp(self): + super().setUp() + + self.analytic_plan = self.env["account.analytic.plan"].create( + { + "name": "Analytic Plan Test", + } + ) + + self.analytic_account_A = self.env["account.analytic.account"].create( + { + "name": "Analytic Account A", + "code": "100001", + "plan_id": self.analytic_plan.id, + } + ) + self.analytic_account_B = self.env["account.analytic.account"].create( + { + "name": "Analytic Account B", + "code": "100002", + "plan_id": self.analytic_plan.id, + } + ) + + # Public Pricelist Test + public_pricelist = self.env["product.pricelist"].create( + {"name": "Public Pricelist", "sequence": 1} + ) + + # Product Created A, B, C + ProductObj = self.env["product.product"] + self.productA = ProductObj.create( + { + "name": "Product A", + "type": "product", + "rental": True, + "rental_of_month": True, + "rental_of_day": True, + "rental_of_hour": True, + "rental_price_month": 1000, + "rental_price_day": 100, + "rental_price_hour": 10, + "income_analytic_account_id": self.analytic_account_A.id, + "expense_analytic_account_id": self.analytic_account_A.id, + "def_pricelist_id": public_pricelist.id, + } + ) + self.productB = ProductObj.create( + { + "name": "Product B", + "type": "product", + "rental": True, + } + ) + self.productC = ProductObj.create( + { + "name": "Product C", + "type": "product", + "rental": True, + } + ) + self.productD = ProductObj.create( + { + "name": "Product D", + "type": "product", + "rental": True, + } + ) + self.productE = ProductObj.create( + { + "name": "Product E", + "type": "product", + "rental": True, + "rental_of_day": True, + "rental_price_day": 500, + "default_code": "PRD-E123", + "def_pricelist_id": public_pricelist.id, + } + ) + self.productF = ProductObj.create( + { + "name": "Product F", + "type": "product", + "rental": True, + "rental_of_hour": True, + "rental_price_hour": 1000, + } + ) + self.today = fields.Date.from_string(fields.Date.today()) + self.tomorrow = self.today + relativedelta(days=1) + self.date_28_day_later = self.today + relativedelta(days=28) + self.date_63_day_later = self.today + relativedelta(days=63) + self.date_one_month_later = self.today + relativedelta(months=1) + self.date_two_month_later = self.today + relativedelta(months=2) + self.date_three_month_later = self.today + relativedelta(months=3) + self.rental_order = ( + self.env["sale.order"] + .with_context( + **{ + "default_type_id": self.rental_sale_type.id, + } + ) + .create( + { + "partner_id": self.partnerA.id, + } + ) + ) + + def test_00_auto_create_service_product(self): + """ + check functions that create the rental service automatically. + services of productA was created by using function create() + services of productB will be created by using function write() + """ + self.productB.write( + { + "rental_of_month": True, + "rental_of_day": True, + "rental_of_hour": True, + "rental_price_month": 2000, + "rental_price_day": 200, + "rental_price_hour": 20, + "income_analytic_account_id": self.analytic_account_B.id, + "expense_analytic_account_id": self.analytic_account_B.id, + } + ) + # check service products of product A + check_hour_A = check_day_A = check_month_A = False + check_income_aa_A = check_expense_aa_A = False + self.assertEqual(len(self.productA.rental_service_ids), 3) + for p in self.productA.rental_service_ids: + if p.uom_id == self.uom_month: + self.assertEqual(p.lst_price, 1000) + check_month_A = True + if p.uom_id == self.uom_day: + self.assertEqual(p.lst_price, 100) + check_day_A = True + if p.uom_id == self.uom_hour: + self.assertEqual(p.lst_price, 10) + check_hour_A = True + if p.income_analytic_account_id == self.productA.income_analytic_account_id: + check_income_aa_A = True + if ( + p.expense_analytic_account_id + == self.productA.expense_analytic_account_id + ): + check_expense_aa_A = True + self.assertTrue(check_hour_A) + self.assertTrue(check_day_A) + self.assertTrue(check_month_A) + self.assertTrue(check_income_aa_A) + self.assertTrue(check_expense_aa_A) + + # check service products of product B + check_hour_B = check_day_B = check_month_B = False + check_income_aa_B = check_expense_aa_B = False + self.assertEqual(len(self.productB.rental_service_ids), 3) + for p in self.productB.rental_service_ids: + if p.uom_id == self.uom_month: + self.assertEqual(p.lst_price, 2000) + check_month_B = True + if p.uom_id == self.uom_day: + self.assertEqual(p.lst_price, 200) + check_day_B = True + if p.uom_id == self.uom_hour: + self.assertEqual(p.lst_price, 20) + check_hour_B = True + if p.income_analytic_account_id == self.productB.income_analytic_account_id: + check_income_aa_B = True + if ( + p.expense_analytic_account_id + == self.productB.expense_analytic_account_id + ): + check_expense_aa_B = True + self.assertTrue(check_hour_B) + self.assertTrue(check_day_B) + self.assertTrue(check_month_B) + self.assertTrue(check_income_aa_B) + self.assertTrue(check_expense_aa_B) + + def test_01_rental_onchange_productA(self): + """ + check onchange functions by setting of display_product_id + check onchange functions by changing of product_uom + """ + line = ( + self.env["sale.order.line"] + .with_context( + **{ + "type_id": self.rental_sale_type.id, + } + ) + .new( + { + "order_id": self.rental_order.id, + "display_product_id": self.productA.id, + "start_date": self.today, + "end_date": self.date_three_month_later, + } + ) + ) + line.onchange_display_product_id() + line._onchange_product_id() + line.onchange_rental() + line._onchange_product_uom() + line.rental_product_id_change() + _run_sol_onchange_date(line) + self.assertEqual(line.rental, True) + self.assertEqual(line.rental_type, "new_rental") + self.assertEqual(line.can_sell_rental, False) + self.assertEqual(line.product_id, self.productA.product_rental_day_id) + self.assertEqual(line.display_product_id, self.productA) + self.assertEqual(line.product_uom, self.uom_day) + self.assertEqual(line.product_uom_qty > 80, True) + self.assertEqual(line.rental_qty, 1) + self.assertEqual(line.number_of_time_unit > 80, True) + _run_sol_onchange_product_uom(line, self.uom_month) + self.assertEqual(line.product_id, self.productA.product_rental_month_id) + self.assertEqual(line.display_product_id, self.productA) + self.assertEqual(line.product_uom, self.uom_month) + self.assertEqual(line.product_uom_qty, 3) + self.assertEqual(line.rental_qty, 1) + self.assertEqual(line.number_of_time_unit, 3) + + def test_02_rental_onchange_productC(self): + """ + check auto detect time_uom "Month" + check onchange functions by changing of can_sell_rental + check onchange functions by changing of rental + """ + self.productC.write( + { + "rental_of_month": True, + } + ) + line = ( + self.env["sale.order.line"] + .with_context( + **{ + "type_id": self.rental_sale_type.id, + } + ) + .new( + { + "order_id": self.rental_order.id, + "display_product_id": self.productC.id, + "start_date": self.today, + "end_date": self.date_three_month_later, + } + ) + ) + line.onchange_display_product_id() + line._onchange_product_id() + line.onchange_rental() + line._onchange_product_uom() + line.rental_product_id_change() + _run_sol_onchange_date(line) + self.assertEqual(line.rental, True) + self.assertEqual(line.rental_type, "new_rental") + self.assertEqual(line.can_sell_rental, False) + self.assertEqual(line.product_id, self.productC.product_rental_month_id) + self.assertEqual(line.display_product_id, self.productC) + self.assertEqual(line.product_uom, self.uom_month) + self.assertEqual(line.product_uom_qty, 3) + self.assertEqual(line.rental_qty, 1) + self.assertEqual(line.number_of_time_unit, 3) + _run_sol_onchange_can_sell_rental(line, True) + self.assertEqual(line.rental, False) + self.assertEqual(line.rental_type, False) + self.assertEqual(line.can_sell_rental, True) + self.assertEqual(line.product_id, self.productC) + self.assertEqual(line.display_product_id, self.productC) + self.assertEqual(line.product_uom, self.uom_unit) + self.assertEqual(line.rental_qty, 0) + _run_sol_onchange_rental(line, True) + self.assertEqual(line.rental, True) + self.assertEqual(line.rental_type, "new_rental") + self.assertEqual(line.can_sell_rental, False) + self.assertEqual(line.product_id, self.productC.product_rental_month_id) + self.assertEqual(line.display_product_id, self.productC) + self.assertEqual(line.product_uom, self.uom_month) + self.assertEqual(line.rental_qty, 1) + + def test_03_rental_pricelist_items(self): + self.productA.write( + { + "day_scale_pricelist_item_ids": [ + ( + 0, + 0, + { + "min_quantity": 20, + "fixed_price": 90, + "applied_on": "0_product_variant", + "compute_price": "fixed", + "product_id": self.productA.product_rental_day_id.id, + "pricelist_id": self.productA.def_pricelist_id.id, + }, + ), + ( + 0, + 0, + { + "min_quantity": 45, + "fixed_price": 80, + "applied_on": "0_product_variant", + "compute_price": "fixed", + "product_id": self.productA.product_rental_day_id.id, + "pricelist_id": self.productA.def_pricelist_id.id, + }, + ), + ], + } + ) + self.productA.write( + { + "month_scale_pricelist_item_ids": [ + ( + 0, + 0, + { + "min_quantity": 2, + "fixed_price": 900, + "applied_on": "0_product_variant", + "compute_price": "fixed", + "product_id": self.productA.product_rental_month_id.id, + "pricelist_id": self.productA.def_pricelist_id.id, + }, + ), + ], + } + ) + item1 = self.env["product.pricelist.item"].create( + { + "applied_on": "0_product_variant", + "compute_price": "fixed", + "product_id": self.productA.product_rental_day_id.id, + "pricelist_id": self.productA.def_pricelist_id.id, + "min_quantity": 80, + "fixed_price": 70, + } + ) + item1._onchange_product_id() + self.assertEqual(item1.day_item_id, self.productA) + item2 = self.env["product.pricelist.item"].create( + { + "applied_on": "0_product_variant", + "compute_price": "fixed", + "product_id": self.productA.product_rental_month_id.id, + "pricelist_id": self.productA.def_pricelist_id.id, + "min_quantity": 3, + "fixed_price": 800, + } + ) + item2._onchange_product_id() + self.assertEqual(item2.month_item_id, self.productA) + + line = ( + self.env["sale.order.line"] + .with_context( + **{ + "type_id": self.rental_sale_type.id, + } + ) + .new( + { + "order_id": self.rental_order.id, + "display_product_id": self.productA.id, + "start_date": self.today, + "end_date": self.date_three_month_later, + } + ) + ) + _run_sol_onchange_display_product_id(line) + _run_sol_onchange_date(line) + self.assertEqual(line.product_uom_qty > 80, True) + self.assertEqual(line.price_unit, 70) + _run_sol_onchange_date(line, end_date=self.date_two_month_later) + self.assertEqual(line.product_uom_qty > 45, True) + self.assertEqual(line.price_unit, 80) + _run_sol_onchange_date(line, end_date=self.date_one_month_later) + self.assertEqual(line.product_uom_qty > 20, True) + self.assertEqual(line.price_unit, 90) + _run_sol_onchange_date(line, end_date=self.tomorrow) + self.assertEqual(line.product_uom_qty, 2) + self.assertEqual(line.price_unit, 100) + _run_sol_onchange_product_uom(line, self.uom_month) + _run_sol_onchange_date(line, end_date=self.date_28_day_later) # check round + self.assertEqual(line.product_uom_qty, 1) + self.assertEqual(line.price_unit, 1000) + _run_sol_onchange_date(line, end_date=self.date_63_day_later) # check round + self.assertEqual(line.product_uom_qty, 2) + self.assertEqual(line.price_unit, 900) + _run_sol_onchange_date(line, end_date=self.date_three_month_later) + self.assertEqual(line.product_uom_qty, 3) + self.assertEqual(line.price_unit, 800) + + def test_04_check_rental_order_line_productD(self): + """ + check function check_rental_order_line() + """ + line = ( + self.env["sale.order.line"] + .with_context( + **{ + "type_id": self.rental_sale_type.id, + } + ) + .new( + { + "order_id": self.rental_order.id, + "display_product_id": self.productD.id, + } + ) + ) + line.onchange_display_product_id() + line._onchange_product_id() + line.rental = True + vals = line._convert_to_write(line._cache) + self.env["sale.order.line"].create(vals) + with self.assertRaises(exceptions.UserError) as e: + self.rental_order.action_confirm() + self.assertEqual( + "The product Product D is not correctly configured.", str(e.exception) + ) + + def test_05_check_rental_productE(self): + self.assertEqual(len(self.productE.rental_service_ids), 1) + rental_serviceE = self.productE.rental_service_ids[0] + # check type and must_have_dates of product + self.assertEqual(rental_serviceE.type, "service") + self.assertEqual(rental_serviceE.must_have_dates, True) + with self.assertRaises(ValidationError) as e: + rental_serviceE.type = "consu" + self.assertEqual( + "The rental product 'Rental of Product E (Day(s))' must be " + "of type 'Service'.", + str(e.exception), + ) + with self.assertRaises(ValidationError) as e: + rental_serviceE.must_have_dates = False + self.assertEqual( + "The rental product 'Rental of Product E (Day(s))'" + " must have the option 'Must Have Start and End Dates' checked.", + str(e.exception), + ) + self.productE.write( + { + "hour_scale_pricelist_item_ids": [ + ( + 0, + 0, + { + "min_quantity": 3, + "fixed_price": 600, + "applied_on": "0_product_variant", + "compute_price": "fixed", + "product_id": self.productE.product_rental_day_id.id, + "pricelist_id": self.productE.def_pricelist_id.id, + }, + ), + ], + } + ) + item1 = self.env["product.pricelist.item"].create( + { + "applied_on": "0_product_variant", + "compute_price": "fixed", + "product_id": self.productE.product_rental_day_id.id, + "pricelist_id": self.productE.def_pricelist_id.id, + "min_quantity": 80, + "fixed_price": 70, + } + ) + item1._onchange_product_id() + self.assertEqual(item1.day_item_id, self.productE) + self.assertEqual(self.productE.default_code, "PRD-E123") + self.assertEqual(rental_serviceE.default_code, "RENT-D-PRD-E123") + self.productE.default_code = "PRD-E110" + self.assertEqual(self.productE.default_code, "PRD-E110") + self.assertEqual(rental_serviceE.default_code, "RENT-D-PRD-E110") + self.productE.name = "Product E1" + self.assertEqual(rental_serviceE.name, "Rental of Product E1 (Day(s))") + self.productE.active = False + self.assertEqual(rental_serviceE.active, False) + self.productE.active = True + self.assertTrue(rental_serviceE.active) + + def test_06_check_start_end_dates_productF(self): + rental_order = ( + self.env["sale.order"] + .with_context( + **{ + "default_type_id": self.rental_sale_type.id, + } + ) + .create( + { + "partner_id": self.partnerA.id, + } + ) + ) + line = ( + self.env["sale.order.line"] + .with_context( + **{ + "type_id": self.rental_sale_type.id, + } + ) + .new( + { + "order_id": rental_order.id, + "display_product_id": self.productF.id, + } + ) + ) + line.onchange_display_product_id() + line._onchange_product_id() + line.onchange_rental() + line._onchange_product_uom() + line.rental_product_id_change() + _run_sol_onchange_date(line) + line._compute_start_date() + line._compute_end_date() + self.assertEqual(line.start_date, False) + self.assertEqual(line.end_date, False) + rental_order.update( + { + "default_start_date": self.today, + "default_end_date": self.tomorrow, + } + ) + line._compute_start_date() + line._compute_end_date() + self.assertEqual(line.start_date, self.today) + self.assertEqual(line.end_date, self.tomorrow) diff --git a/rental_pricelist/views/product_template_view.xml b/rental_pricelist/views/product_template_view.xml new file mode 100644 index 00000000..38684a4c --- /dev/null +++ b/rental_pricelist/views/product_template_view.xml @@ -0,0 +1,173 @@ + + + + + view.product.template.form + product.template + 112 + + + + + + + + + + + + + + + + + + Bulk Prices + + + + + + + + + + + + + + + + + + + + Bulk Prices + + + + + + + + + + + + + + + + + + + + Bulk Prices + + + + + + + + + + + + + + + + + + + diff --git a/rental_pricelist/views/product_view.xml b/rental_pricelist/views/product_view.xml new file mode 100644 index 00000000..8b885037 --- /dev/null +++ b/rental_pricelist/views/product_view.xml @@ -0,0 +1,155 @@ + + + + view.product.product.form + product.product + 112 + + + + + + + + + + + + + + Bulk Prices + + + + + + + + + + + + + + + + + + + + Bulk Prices + + + + + + + + + + + + + + + + + + + + Bulk Prices + + + + + + + + + + + + + + + + + + + diff --git a/rental_pricelist/views/res_company_view.xml b/rental_pricelist/views/res_company_view.xml new file mode 100644 index 00000000..038c4c38 --- /dev/null +++ b/rental_pricelist/views/res_company_view.xml @@ -0,0 +1,71 @@ + + + res.company.form + res.company + + + + + Configure the name and default code of your rental services. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rental_pricelist/views/sale_view.xml b/rental_pricelist/views/sale_view.xml new file mode 100644 index 00000000..c42e67c9 --- /dev/null +++ b/rental_pricelist/views/sale_view.xml @@ -0,0 +1,88 @@ + + + + rental_pricelist.view_order_form + sale.order + + + + 1 + + + + + + 1 + + + + + + + {'invisible': [('rental_ok', '=', False)]} + + + {'type_id': parent.type_id} + + + 1 + + + + + + + + view.sales.order.filter + sale.order + + + + ['|', ('order_line.product_id', 'ilike', self), ('order_line.display_product_id', 'ilike', self)] + + + + + diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..f597596d --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +odoo-addon-rental_base @ git+https://github.com/OCA/vertical-rental.git@refs/pull/53/head#subdirectory=rental_base +odoo-addon-sale_rental @ git+https://github.com/OCA/vertical-rental.git@refs/pull/52/head#subdirectory=sale_rental