diff --git a/rental_variant/README.rst b/rental_variant/README.rst new file mode 100644 index 00000000..f505d43a --- /dev/null +++ b/rental_variant/README.rst @@ -0,0 +1,95 @@ +============== +Rental Variant +============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:607db7f262673589812c58bbef7425a4ea20a148c1917e45ae0a87cef2e08cab + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_variant + :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_variant + :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 module allows to rent variant products. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +* From a physical or consumable product template, create its rental twin (as service) by "Copy For Rental" button +* The attributes and values between the physical and rental service templates are synchronize by a cron and a button "Update Attributes values". +* In a Sale Order, select the service variant to rent, the rental configuration is in a popup on the line. +* In case of dynamical product creation mode on attributes, if the physical product does not exist, it will be created. + + +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 +~~~~~~~ + +* Akretion +* +* Groupe Voltaire SAS + +Contributors +~~~~~~~~~~~~ + +* Kévin Roche + +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. + +.. |maintainer-Kev-Roche| image:: https://github.com/Kev-Roche.png?size=40px + :target: https://github.com/Kev-Roche + :alt: Kev-Roche + +Current `maintainer `__: + +|maintainer-Kev-Roche| + +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_variant/__init__.py b/rental_variant/__init__.py new file mode 100644 index 00000000..9b429614 --- /dev/null +++ b/rental_variant/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/rental_variant/__manifest__.py b/rental_variant/__manifest__.py new file mode 100644 index 00000000..1a557f3a --- /dev/null +++ b/rental_variant/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche + +{ + "name": "Rental Variant", + "version": "16.0.1.0.0", + "category": "Rental", + "website": "https://github.com/OCA/vertical-rental", + "author": "Akretion, Odoo Community Association (OCA), , Groupe Voltaire SAS", + "maintainers": ["Kev-Roche"], + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "sale_rental", + ], + "data": [ + "security/ir.model.access.csv", + "data/data.xml", + "views/product_template.xml", + "views/sale_order.xml", + "wizard/sale_rental_wizard.xml", + ], +} diff --git a/rental_variant/data/data.xml b/rental_variant/data/data.xml new file mode 100644 index 00000000..9fb8ae7c --- /dev/null +++ b/rental_variant/data/data.xml @@ -0,0 +1,15 @@ + + + + Update Rental Attributes Values + + + 5 + minutes + -1 + + + code + model.cron_update_rental_attributes_values() + + diff --git a/rental_variant/i18n/fr.po b/rental_variant/i18n/fr.po new file mode 100644 index 00000000..2bd058a0 --- /dev/null +++ b/rental_variant/i18n/fr.po @@ -0,0 +1,205 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * rental_variant +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-12-14 14:59+0000\n" +"PO-Revision-Date: 2023-12-14 16:04+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.4.1\n" + +#. module: rental_variant +#: model_terms:ir.ui.view,arch_db:rental_variant.sale_rental_line_wizard_view +msgid "Confirm" +msgstr "Confirmer" + +#. module: rental_variant +#: model_terms:ir.ui.view,arch_db:rental_variant.product_template_only_form_view +msgid "Copy For Rental" +msgstr "Copier Pour location" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__create_uid +msgid "Created by" +msgstr "" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__create_date +msgid "Created on" +msgstr "" + +#. module: rental_variant +#: model_terms:ir.ui.view,arch_db:rental_variant.sale_rental_line_wizard_view +msgid "Discard" +msgstr "Annuler" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__display_name +msgid "Display Name" +msgstr "" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__end_date +msgid "End Date" +msgstr "Date de fin" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__id +msgid "ID" +msgstr "" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_order_line__is_rental +msgid "Is Rental Variant Exist" +msgstr "" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard____last_update +msgid "Last Modified on" +msgstr "" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__write_date +msgid "Last Updated on" +msgstr "" + +#. module: rental_variant +#: model:ir.model.fields.selection,name:rental_variant.selection__sale_rental_line_wizard__rental_type__new_rental +msgid "New Rental" +msgstr "Nouvelle location" + +#. module: rental_variant +#. odoo-python +#: code:addons/rental_variant/models/sale_order.py:0 +#, python-format +msgid "Physical rent product can not be found / created." +msgstr "Le produit physique à loué ne peut être trouvé / créé." + +#. module: rental_variant +#: model:ir.model,name:rental_variant.model_product_template +msgid "Product" +msgstr "Article" + +#. module: rental_variant +#: model:ir.model,name:rental_variant.model_product_product +msgid "Product Variant" +msgstr "Variante de produit" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_product_product__rental_product_tmpl_id +#: model:ir.model.fields,field_description:rental_variant.field_product_template__rental_product_tmpl_id +msgid "Related Rental Product Template" +msgstr "Modèle de location associé" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_order_line__rental_id +msgid "Rental" +msgstr "Location" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_product_product__rental_attributes_values_need_update +#: model:ir.model.fields,field_description:rental_variant.field_product_template__rental_attributes_values_need_update +msgid "Rental Attributes Values Need Update" +msgstr "Valeurs d'attributs à Mettre à Jour" + +#. module: rental_variant +#: model_terms:ir.ui.view,arch_db:rental_variant.view_order_form +msgid "Rental Config." +msgstr "Location Config." + +#. module: rental_variant +#: model:ir.model.fields.selection,name:rental_variant.selection__sale_rental_line_wizard__rental_type__rental_extension +msgid "Rental Extension" +msgstr "Exension de location" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__rental_line_id +msgid "Rental Line" +msgstr "" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_product_product__rental_product_id +msgid "Rental Product" +msgstr "Produit Loué" + +#. module: rental_variant +#: model_terms:ir.ui.view,arch_db:rental_variant.product_template_only_form_view +msgid "Rental Product Template" +msgstr "Modèle de location" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__rental_qty +msgid "Rental Qty" +msgstr "Qté à louer" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__rental_type +msgid "Rental Type" +msgstr "Type de location" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__extension_rental_id +msgid "Rental to Extend" +msgstr "Location à étendre" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__sell_rental_id +msgid "Rental to Sell" +msgstr "Location à vendre" + +#. module: rental_variant +#: model_terms:ir.ui.view,arch_db:rental_variant.sale_rental_line_wizard_view +msgid "Rented Product Qty" +msgstr "Qté produit loué" + +#. module: rental_variant +#: model:ir.model,name:rental_variant.model_sale_rental_line_wizard +msgid "Sale Rental Line Wizard" +msgstr "" + +#. module: rental_variant +#: model:ir.model,name:rental_variant.model_sale_order +msgid "Sales Order" +msgstr "Bon de commande" + +#. module: rental_variant +#: model:ir.model,name:rental_variant.model_sale_order_line +msgid "Sales Order Line" +msgstr "Ligne de commande" + +#. module: rental_variant +#: model:ir.model.fields,field_description:rental_variant.field_sale_rental_line_wizard__start_date +msgid "Start Date" +msgstr "Date de début" + +#. module: rental_variant +#: model_terms:ir.ui.view,arch_db:rental_variant.product_template_only_form_view +msgid "Update Attributes values" +msgstr "" + +#. module: rental_variant +#: model:ir.actions.server,name:rental_variant.ir_cron_update_rental_attributes_values_ir_actions_server +#: model:ir.cron,cron_name:rental_variant.ir_cron_update_rental_attributes_values +msgid "Update Rental Attributes Values" +msgstr "MAJ valeurs d'attributs de location" + +#. module: rental_variant +#. odoo-python +#: code:addons/rental_variant/models/product_template.py:0 +#, python-format +msgid "[RENT] %s" +msgstr "[LOC] %s" diff --git a/rental_variant/models/__init__.py b/rental_variant/models/__init__.py new file mode 100644 index 00000000..de7a6835 --- /dev/null +++ b/rental_variant/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_product +from . import product_template +from . import sale_order diff --git a/rental_variant/models/product_product.py b/rental_variant/models/product_product.py new file mode 100644 index 00000000..4089b44c --- /dev/null +++ b/rental_variant/models/product_product.py @@ -0,0 +1,35 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche + +from odoo import fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + rental_product_id = fields.Many2one( + "product.product", + ) + + def get_rented_product_id(self): + if len(self.rented_product_tmpl_id.product_variant_ids) == 1: + return self.rented_product_tmpl_id.product_variant_ids + product = self.rented_product_tmpl_id.product_variant_ids.filtered_domain( + [ + ( + "product_template_attribute_value_ids", + "=", + self.product_template_attribute_value_ids.ids, + ) + ] + ) + if product: + if len(product) == 1: + return product + else: + return product.filtered( + lambda x, self: len(self.product_template_attribute_value_ids) + == len(x.product_template_attribute_value_ids) + ) + else: + return None diff --git a/rental_variant/models/product_template.py b/rental_variant/models/product_template.py new file mode 100644 index 00000000..a3e2eb20 --- /dev/null +++ b/rental_variant/models/product_template.py @@ -0,0 +1,118 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + rental_product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Related Rental Product Template", + ) + + rental_attributes_values_need_update = fields.Boolean( + compute="_compute_rental_attributes_values_need_update", + store=True, + readonly=True, + ) + show_rental_attributes_values_need_update = fields.Boolean( + related="rental_product_tmpl_id.rental_attributes_values_need_update", + ) + + @api.depends("rental_product_tmpl_id") + def _compute_rented_product_tmpl_id(self): + with_variants = self.filtered(lambda template: template.attribute_line_ids) + for template in with_variants: + template.rental_product_tmpl_id.rented_product_tmpl_id = template.id + return super( + ProductTemplate, self - with_variants + )._compute_rented_product_tmpl_id() + + def copy_for_rental(self): + self.ensure_one() + rental_template = self.with_context(copy_for_rental=True).copy() + active_langs = self.env["res.lang"].get_installed() + for lang_code, _lang_name in active_langs: + new_name = "[RENT] %s" % self.with_context(lang=lang_code).name + rental_template.with_context(lang=lang_code).name = new_name + self.rental_product_tmpl_id = rental_template.id + + def write(self, vals): + for rec in self: + if "attribute_line_ids" in vals and rec.rented_product_tmpl_id: + raise UserError( + _( + "You can not change the attributes of a rental " + "product. Instead, please update the rented one." + ) + ) + return super().write(vals) + + @api.model_create_multi + def create(self, values): + templates = super().create(values) + if self.env.context.get("copy_for_rental"): + for template in templates: + template.must_have_dates = True + template.type = "service" + template.uom_id = self.env.ref("uom.product_uom_day").id + return templates + + @api.depends( + "rented_product_tmpl_id.attribute_line_ids", + "rented_product_tmpl_id.attribute_line_ids.value_ids", + ) + def _compute_rental_attributes_values_need_update(self): + for rec in self: + rec.rental_attributes_values_need_update = False + if ( + rec.type == "service" + and rec.rented_product_tmpl_id + and rec.rented_product_tmpl_id.attribute_line_ids.value_ids + != rec.attribute_line_ids.value_ids + ): + rec.rental_attributes_values_need_update = True + + def update_rental_attributes_values(self): + rented_attributes = self.rented_product_tmpl_id.attribute_line_ids.attribute_id + rental_attributes = self.attribute_line_ids.attribute_id + attributes_to_add = rented_attributes - rental_attributes + attributes_to_delete = rental_attributes - rented_attributes + attributes_to_update = ( + rental_attributes - attributes_to_delete + attributes_to_add + ) + + for attribute in attributes_to_add: + self.env["product.template.attribute.line"].create( + { + "product_tmpl_id": self.id, + "attribute_id": attribute.id, + "value_ids": [(6, 0, attribute.value_ids.ids)], + } + ) + for attribute in attributes_to_update: + domain = [("attribute_id", "=", attribute.id)] + line = self.attribute_line_ids.filtered_domain(domain) + rented_line = ( + self.rented_product_tmpl_id.attribute_line_ids.filtered_domain(domain) + ) + if line.value_ids != rented_line.value_ids: + line.write( + { + "value_ids": [(6, 0, rented_line.value_ids.ids)], + } + ) + for attribute in attributes_to_delete: + domain = [ + ("attribute_id", "=", attribute.id), + ("product_tmpl_id", "=", self.id), + ] + self.attribute_line_ids.filtered_domain(domain).unlink() + + def cron_update_rental_attributes_values(self): + templates = self.search([("rental_attributes_values_need_update", "=", True)]) + for template in templates: + template.update_rental_attributes_values() diff --git a/rental_variant/models/sale_order.py b/rental_variant/models/sale_order.py new file mode 100644 index 00000000..e78465a4 --- /dev/null +++ b/rental_variant/models/sale_order.py @@ -0,0 +1,128 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def action_confirm(self): + rentals = [line for line in self.order_line if line.is_rental] + for rental in rentals: + rented = ( + rental.product_id.rented_product_id.id or rental.set_rented_product_id() + ) + if not isinstance(rented, int): + rented = rented.id + rented_product = self.env["product.product"].browse(rented) + rented_product.rental_product_id = rental.product_id + rental.product_id.rented_product_id = rented_product + rental.rental_product_id_change() + return super().action_confirm() + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + rental_id = fields.Many2one( + comodel_name="sale.rental", + string="Rental", + compute="_compute_rental_id", + store=True, + ) + is_rental = fields.Boolean( + string="Is Rental Variant Exist", + compute="_compute_is_rental", + ) + + @api.depends("product_id.product_tmpl_id") + def _compute_is_rental(self): + for rec in self: + rec.is_rental = bool(rec.product_id.product_tmpl_id.rented_product_tmpl_id) + + @api.depends("order_id.state") + def _compute_rental_id(self): + for rec in self: + rec.rental_id = self.env["sale.rental"].search( + [("start_order_line_id", "=", rec.id)], limit=1 + ) + + def set_rented_product_id(self): + if not self.product_id.rented_product_id: + rented_product_tmpl = self.product_id.product_tmpl_id.rented_product_tmpl_id + rented = rented_product_tmpl.product_variant_ids.filtered( + lambda x, s=self: ( + s.product_id.product_template_variant_value_ids.product_attribute_value_id + == x.product_template_variant_value_ids.product_attribute_value_id + ) + ).id + if not rented: + rented_ptav = self.env["product.template.attribute.value"].search( + [ + ("product_tmpl_id", "=", rented_product_tmpl.id), + ( + "product_attribute_value_id", + "in", + self.product_template_attribute_value_idsproduct_attribute_value_id.ids, + ), + ] + ) + + rented = rented_product_tmpl.create_product_variant(rented_ptav.ids) + if not rented: + raise ValidationError( + _("Physical rent product " "can not be found / created.") + ) + else: + return rented + + def rental_product_id_change(self): + if ( + not self.product_id.rented_product_id + and self.product_id.product_tmpl_id.rented_product_tmpl_id + ): + return + else: + return super().rental_product_id_change() + + def _check_sale_line_rental(self): + lines = [ + line + for line in self + if line.rental_type == "new_rental" + and not line.product_id.rented_product_id + and line.product_id.product_tmpl_id.rented_product_tmpl_id + ] + return super(SaleOrderLine, self - lines).rental_product_id_change() + + @api.constrains("is_rental") + def _check_start_end_dates(self): + for line in self: + if line.is_rental and not line.start_date and not line.end_date: + return + else: + return super()._check_start_end_dates() + + def open_rental_line_wizard(self): + view_id = self.env.ref("rental_variant.sale_rental_line_wizard_view").id + wizard = self.env["sale.rental.line.wizard"].create( + { + "rental_line_id": self.id, + "rental_type": self.rental_type, + "extension_rental_id": self.extension_rental_id.id, + "rental_qty": self.rental_qty, + "sell_rental_id": self.sell_rental_id.id, + "start_date": self.start_date, + "end_date": self.end_date, + } + ) + return { + "type": "ir.actions.act_window", + "res_model": "sale.rental.line.wizard", + "res_id": wizard.id, + "view_mode": "form", + "view_id": view_id, + "target": "new", + } diff --git a/rental_variant/readme/CONTRIBUTORS.rst b/rental_variant/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..5ef178f7 --- /dev/null +++ b/rental_variant/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Kévin Roche \ No newline at end of file diff --git a/rental_variant/readme/DESCRIPTION.rst b/rental_variant/readme/DESCRIPTION.rst new file mode 100644 index 00000000..0f28fd35 --- /dev/null +++ b/rental_variant/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows to rent variant products. diff --git a/rental_variant/readme/USAGE.rst b/rental_variant/readme/USAGE.rst new file mode 100644 index 00000000..85922092 --- /dev/null +++ b/rental_variant/readme/USAGE.rst @@ -0,0 +1,5 @@ +* From a physical or consumable product template, create its rental twin (as service) by "Copy For Rental" button +* The attributes and values between the physical and rental service templates are synchronize by a cron and a button "Update Attributes values". +* In a Sale Order, select the service variant to rent, the rental configuration is in a popup on the line. +* In case of dynamical product creation mode on attributes, if the physical product does not exist, it will be created. + diff --git a/rental_variant/security/ir.model.access.csv b/rental_variant/security/ir.model.access.csv new file mode 100644 index 00000000..efa9d08e --- /dev/null +++ b/rental_variant/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_rental_line_wizard,sale.rental.line.wizard,model_sale_rental_line_wizard,sales_team.group_sale_manager,1,1,1,1 diff --git a/rental_variant/static/description/index.html b/rental_variant/static/description/index.html new file mode 100644 index 00000000..b21d2c1e --- /dev/null +++ b/rental_variant/static/description/index.html @@ -0,0 +1,435 @@ + + + + + + +Rental Variant + + + +
+

Rental Variant

+ + +

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

+

This module allows to rent variant products.

+

Table of contents

+ +
+

Usage

+
    +
  • From a physical or consumable product template, create its rental twin (as service) by “Copy For Rental” button
  • +
  • The attributes and values between the physical and rental service templates are synchronize by a cron and a button “Update Attributes values”.
  • +
  • In a Sale Order, select the service variant to rent, the rental configuration is in a popup on the line.
  • +
  • In case of dynamical product creation mode on attributes, if the physical product does not exist, it will be created.
  • +
+
+
+

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

+
    +
  • Akretion
  • +
  • +
  • Groupe Voltaire SAS
  • +
+
+
+

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.

+

Current maintainer:

+

Kev-Roche

+

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_variant/tests/__init__.py b/rental_variant/tests/__init__.py new file mode 100644 index 00000000..0d14503e --- /dev/null +++ b/rental_variant/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_rental_variant diff --git a/rental_variant/tests/test_sale_rental_variant.py b/rental_variant/tests/test_sale_rental_variant.py new file mode 100644 index 00000000..48612400 --- /dev/null +++ b/rental_variant/tests/test_sale_rental_variant.py @@ -0,0 +1,188 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche + +from odoo.tests.common import TransactionCase + + +class TestSaleRentalVariant(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.rented_template = cls.env.ref("product.product_product_4_product_template") + cls.rented_template.copy_for_rental() + cls.rental_template = cls.rented_template.rental_product_tmpl_id + cls.rental_product = cls.rental_template.product_variant_ids[0] + cls.sale = cls.env["sale.order"].create( + { + "partner_id": cls.env.ref("base.res_partner_1").id, + "order_line": [ + ( + 0, + 0, + { + "product_id": cls.rental_product.id, + "product_uom_qty": 10, + "start_date": "2049-01-01", + "end_date": "2049-01-10", + "rental_qty": 1, + }, + ) + ], + } + ) + + cls.rented_dynamic_template = cls.env["product.template"].create( + { + "name": "screw", + "detailed_type": "product", + } + ) + cls.size_attr = cls.env["product.attribute"].create( + { + "name": "Size", + "create_variant": "dynamic", + } + ) + cls.value_small = cls.env["product.attribute.value"].create( + {"name": "Small", "attribute_id": cls.size_attr.id} + ) + cls.value_medium = cls.env["product.attribute.value"].create( + {"name": "Medium", "attribute_id": cls.size_attr.id} + ) + cls.env["product.template.attribute.line"].create( + [ + { + "product_tmpl_id": cls.rented_dynamic_template.id, + "attribute_id": cls.size_attr.id, + "value_ids": [(6, 0, cls.size_attr.value_ids.ids)], + } + ] + ) + cls.rented_dynamic_template.copy_for_rental() + cls.rental_dynamic_template = cls.rented_dynamic_template.rental_product_tmpl_id + cls.value = cls.env["product.template.attribute.value"].search( + [ + ("product_attribute_value_id", "=", cls.value_medium.id), + ("attribute_id", "=", cls.size_attr.id), + ("product_tmpl_id", "=", cls.rental_dynamic_template.id), + ], + limit=1, + ) + cls.env["product.product"].create( + { + "product_tmpl_id": cls.rental_dynamic_template.id, + "product_template_attribute_value_ids": [(6, 0, cls.value.ids)], + } + ) + cls.rental_dynamic_product = cls.rental_dynamic_template.product_variant_ids[0] + cls.sale_dynamic = cls.env["sale.order"].create( + { + "partner_id": cls.env.ref("base.res_partner_1").id, + "order_line": [ + ( + 0, + 0, + { + "product_id": cls.rental_dynamic_product.id, + "product_uom_qty": 10, + "start_date": "2049-01-01", + "end_date": "2049-01-10", + "rental_qty": 1, + }, + ) + ], + } + ) + + def test_copy_for_rental(self): + self.assertEqual( + self.rental_template, self.rented_template.rental_product_tmpl_id + ) + self.assertEqual(self.rental_template.detailed_type, "service") + self.assertEqual( + self.rental_template.attribute_line_ids.value_ids, + self.rented_template.attribute_line_ids.value_ids, + ) + + def test_update_rental_attributes_values(self): + self.assertFalse(self.rental_template.rental_attributes_values_need_update) + self.env["product.template.attribute.line"].create( + { + "product_tmpl_id": self.rented_template.id, + "attribute_id": self.env.ref("product.product_attribute_3").id, + "value_ids": [ + ( + 6, + 0, + [ + self.env.ref("product.product_attribute_value_5").id, + self.env.ref("product.product_attribute_value_6").id, + ], + ) + ], + } + ) + self.assertTrue(self.rental_template.rental_attributes_values_need_update) + self.rental_template.update_rental_attributes_values() + self.assertFalse(self.rental_template.rental_attributes_values_need_update) + self.assertTrue(self.rental_template.attribute_line_ids.value_ids) + self.assertEqual( + self.rental_template.attribute_line_ids.value_ids, + self.rented_template.attribute_line_ids.value_ids, + ) + + def test_link_to_rented_variant_in_sale(self): + self.sale.action_confirm() + rental = self.env["sale.rental"].search( + [("start_order_line_id", "=", self.sale.order_line[0].id)] + ) + self.assertEqual(rental.rental_product_id, self.rental_product) + self.assertEqual(rental.rented_product_id.product_tmpl_id, self.rented_template) + + def test_link_to_dynamic_rented_variant_in_sale(self): + self.assertFalse(self.rented_dynamic_template.product_variant_ids) + self.sale_dynamic.action_confirm() + rental = self.env["sale.rental"].search( + [("start_order_line_id", "=", self.sale_dynamic.order_line[0].id)] + ) + self.assertEqual(rental.rental_product_id, self.rental_dynamic_product) + self.assertEqual( + rental.rented_product_id.product_tmpl_id, self.rented_dynamic_template + ) + + def test_sale_rental_wizard(self): + sale = self.env["sale.order"].create( + { + "partner_id": self.env.ref("base.res_partner_1").id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.rental_product.id, + }, + ) + ], + } + ) + + wizard_model = self.env["sale.rental.line.wizard"] + wiz = wizard_model.create( + { + "rental_line_id": sale.order_line.id, + } + ) + wiz.rental_type = "new_rental" + wiz.start_date = "2049-01-01" + wiz.end_date = "2049-01-10" + wiz.rental_qty = 1 + wiz.confirm_rental_config() + self.assertEqual(sale.order_line.product_uom_qty, 10) + + sale.action_confirm() + rental = self.env["sale.rental"].search( + [("start_order_line_id", "=", sale.order_line.id)] + ) + self.assertEqual(rental.rental_product_id, self.rental_product) + self.assertEqual(rental.rented_product_id.product_tmpl_id, self.rented_template) diff --git a/rental_variant/views/product_template.xml b/rental_variant/views/product_template.xml new file mode 100644 index 00000000..319b455d --- /dev/null +++ b/rental_variant/views/product_template.xml @@ -0,0 +1,66 @@ + + + + + rental.product.template.only.form + product.template + + + + {'invisible': [('type', '!=', 'service')]} + + + {'invisible': [('type', '!=', 'service')]} + + +
+ + +
+
+ + +
+
+
diff --git a/rental_variant/views/sale_order.xml b/rental_variant/views/sale_order.xml new file mode 100644 index 00000000..95772bb1 --- /dev/null +++ b/rental_variant/views/sale_order.xml @@ -0,0 +1,69 @@ + + + + + sale_rental.view_order_form + sale.order + + + + bottom + + + {'invisible': [('must_have_dates', '=', False)]} + 1 + + + {'invisible': [('must_have_dates', '=', False)]} + 1 + + + {'invisible': [('must_have_dates', '=', False)]} + 1 + + + +