diff --git a/setup/website_sale_product_compatibility/odoo/addons/website_sale_product_compatibility b/setup/website_sale_product_compatibility/odoo/addons/website_sale_product_compatibility new file mode 120000 index 000000000..28d29d72f --- /dev/null +++ b/setup/website_sale_product_compatibility/odoo/addons/website_sale_product_compatibility @@ -0,0 +1 @@ +../../../../website_sale_product_compatibility \ No newline at end of file diff --git a/setup/website_sale_product_compatibility/setup.py b/setup/website_sale_product_compatibility/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/website_sale_product_compatibility/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/website_sale_product_contract_gift/odoo/addons/website_sale_product_contract_gift b/setup/website_sale_product_contract_gift/odoo/addons/website_sale_product_contract_gift new file mode 120000 index 000000000..e881bbfaa --- /dev/null +++ b/setup/website_sale_product_contract_gift/odoo/addons/website_sale_product_contract_gift @@ -0,0 +1 @@ +../../../../website_sale_product_contract_gift \ No newline at end of file diff --git a/setup/website_sale_product_contract_gift/setup.py b/setup/website_sale_product_contract_gift/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/website_sale_product_contract_gift/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/website_sale_product_compatibility/README.rst b/website_sale_product_compatibility/README.rst new file mode 100644 index 000000000..aa20f1c14 --- /dev/null +++ b/website_sale_product_compatibility/README.rst @@ -0,0 +1,75 @@ +================================== +Website Sale Product Compatibility +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a25ed6054ea1e7db1fe6f90cdb8102edd80859c7504cc1cebdaae1a0123c4819 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-coopiteasy%2Faddons-lightgray.png?logo=github + :target: https://github.com/coopiteasy/addons/tree/16.0/website_sale_product_compatibility + :alt: coopiteasy/addons + +|badge1| |badge2| |badge3| + +This is a utility module that can be used by other modules to check +whether different products are allowed to be added to the same +e-commerce sale order. + +This module works only if the user is redirected to the basket after +adding a product to it. If not no message will be displayed to the user. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Coop IT Easy SC + +Contributors +~~~~~~~~~~~~ + +* `Coop IT Easy SC `_: + + * Rémy Taymans + +Maintainers +~~~~~~~~~~~ + +.. |maintainer-remytms| image:: https://github.com/remytms.png?size=40px + :target: https://github.com/remytms + :alt: remytms + +Current maintainer: + +|maintainer-remytms| + +This module is part of the `coopiteasy/addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/website_sale_product_compatibility/__init__.py b/website_sale_product_compatibility/__init__.py new file mode 100644 index 000000000..4ece112b6 --- /dev/null +++ b/website_sale_product_compatibility/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later +from . import models +from . import controllers diff --git a/website_sale_product_compatibility/__manifest__.py b/website_sale_product_compatibility/__manifest__.py new file mode 100644 index 000000000..55cfc2c06 --- /dev/null +++ b/website_sale_product_compatibility/__manifest__.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +{ + "name": "Website Sale Product Compatibility", + "summary": """ + Generic module to add compatibility check between products.""", + "version": "16.0.1.0.0", + "category": "E-Commerce", + "website": "https://github.com/coopiteasy/addons", + "author": "Coop IT Easy SC", + "maintainers": ["remytms"], + "license": "AGPL-3", + "application": False, + "depends": [ + "website_sale", + ], + "data": [ + "views/templates.xml", + ], +} diff --git a/website_sale_restrict_sepa_dd/controllers/__init__.py b/website_sale_product_compatibility/controllers/__init__.py similarity index 100% rename from website_sale_restrict_sepa_dd/controllers/__init__.py rename to website_sale_product_compatibility/controllers/__init__.py diff --git a/website_sale_restrict_sepa_dd/controllers/main.py b/website_sale_product_compatibility/controllers/main.py similarity index 97% rename from website_sale_restrict_sepa_dd/controllers/main.py rename to website_sale_product_compatibility/controllers/main.py index 02df3d6f5..ffc3e1c38 100644 --- a/website_sale_restrict_sepa_dd/controllers/main.py +++ b/website_sale_product_compatibility/controllers/main.py @@ -8,7 +8,7 @@ from odoo.addons.website_sale.controllers.main import WebsiteSale -class WebsiteSaleRestrictSEPADD(WebsiteSale): +class WebsiteSaleProductCompatibility(WebsiteSale): @http.route( ["/shop/cart/update"], type="http", diff --git a/website_sale_product_compatibility/models/__init__.py b/website_sale_product_compatibility/models/__init__.py new file mode 100644 index 000000000..f28e0f0e1 --- /dev/null +++ b/website_sale_product_compatibility/models/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later +from . import sale_order diff --git a/website_sale_product_compatibility/models/sale_order.py b/website_sale_product_compatibility/models/sale_order.py new file mode 100644 index 000000000..e5fb31e96 --- /dev/null +++ b/website_sale_product_compatibility/models/sale_order.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + + +from odoo import models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def check_product_compatibility(self, product_id): + """Override this function to detect incompatibility between + products. + + Warning: self.ensure_one() + + If an empty string is returned then the product_id is compatible + and will be added to the sale order. If a warning string is + returned then the product is considered not compatible with the + order and the product will not be added to the order. + + By default this method always return no warning message. + + :product_id: The id of the product to check compatibility. + :rtype: str + :return: warning message to be shown on the web interface. + """ + self.ensure_one() + return "" + + def _cart_update(self, product_id, line_id=None, add_qty=0, set_qty=0, **kwargs): + """Prevent incompatible product to be added to the cart.""" + self.ensure_one() + warning = self.check_product_compatibility(product_id) + if warning: + add_qty = None + set_qty = 0 + values = super()._cart_update( + product_id=product_id, + line_id=line_id, + add_qty=add_qty, + set_qty=set_qty, + **kwargs, + ) + values["warning"] = warning + return values diff --git a/website_sale_product_compatibility/readme/CONTRIBUTORS.rst b/website_sale_product_compatibility/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..31498d266 --- /dev/null +++ b/website_sale_product_compatibility/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Coop IT Easy SC `_: + + * Rémy Taymans diff --git a/website_sale_product_compatibility/readme/DESCRIPTION.rst b/website_sale_product_compatibility/readme/DESCRIPTION.rst new file mode 100644 index 000000000..8d6b6af8e --- /dev/null +++ b/website_sale_product_compatibility/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This is a utility module that can be used by other modules to check +whether different products are allowed to be added to the same +e-commerce sale order. + +This module works only if the user is redirected to the basket after +adding a product to it. If not no message will be displayed to the user. diff --git a/website_sale_product_compatibility/static/description/index.html b/website_sale_product_compatibility/static/description/index.html new file mode 100644 index 000000000..1dd8f6f78 --- /dev/null +++ b/website_sale_product_compatibility/static/description/index.html @@ -0,0 +1,425 @@ + + + + + +Website Sale Product Compatibility + + + +
+

Website Sale Product Compatibility

+ + +

Beta License: AGPL-3 coopiteasy/addons

+

This is a utility module that can be used by other modules to check +whether different products are allowed to be added to the same +e-commerce sale order.

+

This module works only if the user is redirected to the basket after +adding a product to it. If not no message will be displayed to the user.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Coop IT Easy SC
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

Current maintainer:

+

remytms

+

This module is part of the coopiteasy/addons project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/website_sale_restrict_sepa_dd/views/templates.xml b/website_sale_product_compatibility/views/templates.xml similarity index 100% rename from website_sale_restrict_sepa_dd/views/templates.xml rename to website_sale_product_compatibility/views/templates.xml diff --git a/website_sale_product_contract_gift/README.rst b/website_sale_product_contract_gift/README.rst new file mode 100644 index 000000000..3524071f1 --- /dev/null +++ b/website_sale_product_contract_gift/README.rst @@ -0,0 +1,70 @@ +================================== +Website Sale Product Contract Gift +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a25ed6054ea1e7db1fe6f90cdb8102edd80859c7504cc1cebdaae1a0123c4819 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-coopiteasy%2Faddons-lightgray.png?logo=github + :target: https://github.com/coopiteasy/addons/tree/16.0/website_sale_product_contract_gift + :alt: coopiteasy/addons + +|badge1| |badge2| |badge3| + +Configure product contract to be a gift to someone else. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Coop IT Easy SC + +Contributors +~~~~~~~~~~~~ + +* `Coop IT Easy SC `_: + + * Rémy Taymans + +Maintainers +~~~~~~~~~~~ + +.. |maintainer-remytms| image:: https://github.com/remytms.png?size=40px + :target: https://github.com/remytms + :alt: remytms + +Current maintainer: + +|maintainer-remytms| + +This module is part of the `coopiteasy/addons `_ project on GitHub. + +You are welcome to contribute. diff --git a/website_sale_product_contract_gift/__init__.py b/website_sale_product_contract_gift/__init__.py new file mode 100644 index 000000000..4ece112b6 --- /dev/null +++ b/website_sale_product_contract_gift/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later +from . import models +from . import controllers diff --git a/website_sale_product_contract_gift/__manifest__.py b/website_sale_product_contract_gift/__manifest__.py new file mode 100644 index 000000000..8083bf4c5 --- /dev/null +++ b/website_sale_product_contract_gift/__manifest__.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +{ + "name": "Website Sale Product Contract Gift", + "summary": """ + Configure product contract to be a gift to someone else.""", + "version": "16.0.1.0.0", + "category": "Website", + "website": "https://github.com/coopiteasy/addons", + "author": "Coop IT Easy SC", + "maintainers": ["remytms"], + "license": "AGPL-3", + "application": False, + "depends": [ + "website_sale_product_compatibility", + "product_contract", + "delivery", + "account_payment_sale", + ], + "data": [ + "views/product_views.xml", + "views/templates.xml", + ], + "assets": { + "web.assets_frontend": [ + "website_sale_product_contract_gift/static/src/js/website_sale.esm.js", + ], + }, +} diff --git a/website_sale_product_contract_gift/controllers/__init__.py b/website_sale_product_contract_gift/controllers/__init__.py new file mode 100644 index 000000000..b71b9fcad --- /dev/null +++ b/website_sale_product_contract_gift/controllers/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later +from . import main diff --git a/website_sale_product_contract_gift/controllers/main.py b/website_sale_product_contract_gift/controllers/main.py new file mode 100644 index 000000000..1af8be1fe --- /dev/null +++ b/website_sale_product_contract_gift/controllers/main.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import date + +from odoo import _, http +from odoo.http import request + +from odoo.addons.website_sale.controllers.main import WebsiteSale + + +class WebsiteSaleIsGift(WebsiteSale): + @http.route( + ["/shop/address"], + type="http", + methods=["GET", "POST"], + auth="public", + website=True, + sitemap=False, + ) + def address(self, **kw): + order = request.website.sale_get_order() + # Ensure user go back to checkout after editing an address + if order.is_gift: + kw["callback"] = "/shop/checkout" + return super().address(**kw) + + @http.route( + ["/shop/checkout"], type="http", auth="public", website=True, sitemap=False + ) + def checkout(self, **post): + """Prevent express checkout if order is a gift and process + form""" + order = request.website.sale_get_order() + errors = {} + if order.is_gift: + # disable express checkout + if "express" in post: + del post["express"] + + # if form is sent + if request.httprequest.method == "POST": + # Check form + vals, errors = self.validate_gift_form_order_line(post) + # Check that shipping address has an email + if not order.partner_shipping_id.email: + errors["email"] = _( + "The receiver of the gift must have an email address defined." + ) + if not errors: + order.order_line.filtered(lambda r: r.product_id.is_gift).write( + vals + ) + return request.redirect("/shop/confirm_order") + result = super().checkout(**post) + result.qcontext["errors"] = errors + result.qcontext["gift_date"] = post.get("gift_date", order.gift_date) + return result + + def validate_gift_form_order_line(self, data): + """Validate the gift form, and return vals for order line""" + errors = {} + vals = {} + gift_date = data.get("gift_date") + if gift_date: + try: + gift_date = date.fromisoformat(data.get("gift_date", "")) + except ValueError: + errors["gift_date"] = _("Date is not valid.") + vals["date_start"] = gift_date + else: + errors["gift_date"] = _("Date is required.") + return vals, errors diff --git a/website_sale_product_contract_gift/models/__init__.py b/website_sale_product_contract_gift/models/__init__.py new file mode 100644 index 000000000..8dc633ba2 --- /dev/null +++ b/website_sale_product_contract_gift/models/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later +from . import product_template +from . import sale_order +from . import sale_order_line +from . import contract +from . import account_move diff --git a/website_sale_product_contract_gift/models/account_move.py b/website_sale_product_contract_gift/models/account_move.py new file mode 100644 index 000000000..e95302eab --- /dev/null +++ b/website_sale_product_contract_gift/models/account_move.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + + +from odoo import models +from odoo.tools import email_normalize + + +class AccountMove(models.Model): + _inherit = "account.move" + + def create_user_for_gift(self): + for invoice in self: + partner_id = invoice.partner_id + is_gift = any( + invoice.line_ids.contract_line_id.contract_id.mapped("is_gift") + ) + if not partner_id.user_id and is_gift: + # TODO: send email to the reciever of the gift + self.env["res.users"].with_context( + no_reset_password=True + )._create_user_from_template( + { + "email": email_normalize(partner_id.email), + "login": email_normalize(partner_id.email), + "partner_id": partner_id.id, + } + ) diff --git a/website_sale_product_contract_gift/models/contract.py b/website_sale_product_contract_gift/models/contract.py new file mode 100644 index 000000000..0dcba2bbe --- /dev/null +++ b/website_sale_product_contract_gift/models/contract.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + + +from odoo import fields, models + + +class ContractContract(models.Model): + _inherit = "contract.contract" + + is_gift = fields.Boolean() + + def _recurring_create_invoice(self, date_ref=False): + invoices = super()._recurring_create_invoice(date_ref=date_ref) + invoices.create_user_for_gift() + return invoices diff --git a/website_sale_product_contract_gift/models/product_template.py b/website_sale_product_contract_gift/models/product_template.py new file mode 100644 index 000000000..70061e308 --- /dev/null +++ b/website_sale_product_contract_gift/models/product_template.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_gift = fields.Boolean() diff --git a/website_sale_product_contract_gift/models/sale_order.py b/website_sale_product_contract_gift/models/sale_order.py new file mode 100644 index 000000000..704daf51e --- /dev/null +++ b/website_sale_product_contract_gift/models/sale_order.py @@ -0,0 +1,210 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + + +import datetime + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + is_gift = fields.Boolean(compute="_compute_is_gift") + gift_date = fields.Date(compute="_compute_gift_date") + + @api.depends("order_line") + def _compute_gift_date(self): + """Get gift_date from date_start on order_line""" + for order in self: + dates = order.order_line.filtered(lambda r: r.product_id.is_gift).mapped( + "date_start" + ) + order.gift_date = dates[0] if dates else False + + @api.depends("order_line") + def _compute_is_gift(self): + """Tell if an order is a gift or not""" + for order in self: + order.is_gift = any(order.order_line.product_id.mapped("is_gift")) + + @api.constrains("order_line") + def _check_gift_alone(self): + """Ensure gift are not mixed in a sale order.""" + for order in self: + lines_to_check = order._exclude_delivery_order_line() + if lines_to_check: + if not self._is_product_compatible(lines_to_check.product_id): + raise ValidationError( + _( + "Cannot add product gift in an order that " + "contains other product that are not gifts." + ) + ) + + @api.model + def _is_product_compatible(self, product_ids): + at_least_one_gift = any(product_ids.mapped("is_gift")) + is_all_gift = all(product_ids.mapped("is_gift")) + return is_all_gift or not at_least_one_gift + + def _exclude_delivery_order_line(self): + """Return the order_lines that are not delivery""" + self.ensure_one() + return self.order_line.filtered(lambda r: not r.is_delivery) + + def check_product_compatibility(self, product_id): + warning = super().check_product_compatibility(product_id) + lines_to_check = self._exclude_delivery_order_line() + product = self.env["product.product"].browse(product_id).exists() + if not warning and lines_to_check and product: + is_product_compatible = self._is_product_compatible( + lines_to_check.product_id | product + ) + if not is_product_compatible: + if product.is_gift: + warning = _( + f"Product {product.name} cannot be added because " + "it's a gift and gift must be purchase seperatly." + ) + else: + warning = _( + f"Product {product.name} cannot be added because " + "it is not a gift and other product are gifts." + ) + return warning + + def action_confirm(self): + """Create contract for gift product""" + contract_model = self.env["contract.contract"] + for order in self.filtered("is_gift"): + line_to_create_contract = order.order_line.filtered( + lambda r: not r.contract_id and r.product_id.is_gift + ) + line_to_update_contract = order.order_line.filtered( + lambda r: ( + r.contract_id + and r.product_id.is_gift + and r not in r.contract_id.contract_line_ids.sale_order_line_id + ) + ) + existing_partners = self.find_contact_partners(order.partner_shipping_id) + if existing_partners: + contract_partner = existing_partners[0] + else: + contract_partner = order.partner_shipping_id.copy( + { + # copy name to prevent odoo adding '(copy)' + # after the name. + "name": order.partner_shipping_id.name, + "type": "contact", + "parent_id": False, + } + ) + for line in line_to_create_contract: + date_start = line.date_start + try: + date_end = datetime.date( + year=date_start.year + 1, + month=date_start.month, + day=date_start.day, + ) + except ValueError: + # ValueError are raised if the date does not exist. + # E.g. 29 february of a non leap year + date_end = datetime.date( + year=date_start.year + 1, + month=date_start.month, + day=date_start.day - 1, + ) + contract = contract_model.create( + { + "name": f"{line.product_id.name}: {order.name}", + "partner_id": contract_partner.id, + "contract_type": "sale", + "is_gift": True, + "date_start": order.gift_date, + "payment_mode_id": order.payment_mode_id.id, + "line_recurrence": True, + "recurring_rule_type": "yearly", + "recurring_interval": 1, + "contract_line_ids": [ + fields.Command.create( + { + "sale_order_line_id": line.id, + "product_id": line.product_id.id, + "name": line.product_id.name, + "quantity": line.product_uom_qty, + "uom_id": line.product_uom.id, + "price_unit": 0, + "date_start": date_start, + "date_end": date_end, + "recurring_rule_type": "yearly", + "recurring_interval": 1, + "auto_renew_rule_type": "yearly", + "auto_renew_interval": "1", + "recurring_next_date": line.date_start, + } + ), + ], + } + ) + contract._onchange_contract_type() + # Cannot use "contract_id" field on order_line because + # for a gift the partner on sale.order is not the same + # as the partner on the contract. + line.write({"gift_contract_id": contract.id}) + for line in line_to_update_contract: + line.create_contract_line(line.contract_id) + return super().action_confirm() + + @api.model + def find_contact_partners(self, partner): + """Find partners that are the same as :partner:""" + partners_found = self.env["res.partner"].search( + [ + ("email", "=", partner.email), + ("type", "=", "contact"), + ] + ) + result = partners_found + fields_to_compare = [ + "country_id", + "zip", + "name", + "user_id", + ] + + # If there is several results we try to eliminate some results. + # We look for result with the same country_id. + # If there is no match with contry_id, then we try to filter for + # another field. + # If there is several matches with country_id, the we try to + # filter with another field. + # If there is only one match with contry_id, then we return this + # result. + # + # At the end, if there is several result that we cannot filter + # more than we return all theses results + if result: + for field in fields_to_compare: + filtered_partners = result.filtered( + lambda r: r[field] == partner[field] + ) + nb_filtered_partners = len(filtered_partners) + if nb_filtered_partners > 0: + result = filtered_partners + if nb_filtered_partners == 1: + break + + return result + + @api.model + def cron_autoconfirm_gift_order(self): + """Auto confirm gift order""" + orders = self.env["sale.order"].search( + [("state", "=", "sent"), ("is_gift", "=", True)] + ) + orders.action_confirm() diff --git a/website_sale_product_contract_gift/models/sale_order_line.py b/website_sale_product_contract_gift/models/sale_order_line.py new file mode 100644 index 000000000..bdc9abf6e --- /dev/null +++ b/website_sale_product_contract_gift/models/sale_order_line.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + gift_contract_id = fields.Many2one( + comodel_name="contract.contract", string="Gift Contract", copy=False + ) diff --git a/website_sale_product_contract_gift/readme/CONTRIBUTORS.rst b/website_sale_product_contract_gift/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..31498d266 --- /dev/null +++ b/website_sale_product_contract_gift/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Coop IT Easy SC `_: + + * Rémy Taymans diff --git a/website_sale_product_contract_gift/readme/DESCRIPTION.rst b/website_sale_product_contract_gift/readme/DESCRIPTION.rst new file mode 100644 index 000000000..681a7eca0 --- /dev/null +++ b/website_sale_product_contract_gift/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Configure product contract to be a gift to someone else. diff --git a/website_sale_product_contract_gift/static/description/index.html b/website_sale_product_contract_gift/static/description/index.html new file mode 100644 index 000000000..58dc91a01 --- /dev/null +++ b/website_sale_product_contract_gift/static/description/index.html @@ -0,0 +1,421 @@ + + + + + +Website Sale Product Contract Gift + + + +
+

Website Sale Product Contract Gift

+ + +

Beta License: AGPL-3 coopiteasy/addons

+

Configure product contract to be a gift to someone else.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Coop IT Easy SC
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

Current maintainer:

+

remytms

+

This module is part of the coopiteasy/addons project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/website_sale_product_contract_gift/static/src/js/website_sale.esm.js b/website_sale_product_contract_gift/static/src/js/website_sale.esm.js new file mode 100644 index 000000000..a8ea7d439 --- /dev/null +++ b/website_sale_product_contract_gift/static/src/js/website_sale.esm.js @@ -0,0 +1,24 @@ +/** @odoo-module **/ + +import publicWidget from "@web/legacy/js/public/public_widget"; + +publicWidget.registry.websiteSaleCartGift = publicWidget.Widget.extend({ + selector: ".oe_website_sale .oe_cart", + events: { + "click #gift_form_confirm_button": "_onClickConfirm", + }, + + // -------------------------------------------------------------------------- + // Handlers + // -------------------------------------------------------------------------- + + /** + * @private + */ + _onClickConfirm: function () { + var $form = $("form[name='gift_date']"); + $form.submit(); + }, +}); + +export default publicWidget.registry.websiteSaleCartGift; diff --git a/website_sale_product_contract_gift/tests/__init__.py b/website_sale_product_contract_gift/tests/__init__.py new file mode 100644 index 000000000..3e2a43d36 --- /dev/null +++ b/website_sale_product_contract_gift/tests/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later +from . import test_gift diff --git a/website_sale_product_contract_gift/tests/test_gift.py b/website_sale_product_contract_gift/tests/test_gift.py new file mode 100644 index 000000000..8e328c253 --- /dev/null +++ b/website_sale_product_contract_gift/tests/test_gift.py @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + + +from odoo import Command, fields +from odoo.tests import common + + +class TestGiftContractBase(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.today = fields.Date.today() + cls.partner = cls.env["res.partner"].create( + { + "name": "partner test contract", + "email": "demo@test.com", + } + ) + cls.partner_gift_to = cls.env["res.partner"].create( + { + "name": "partner gift to", + "email": "partner_gift_to@test.com", + "type": "delivery", + } + ) + cls.product_1 = cls.env.ref("product.product_product_1") + cls.product_1.is_gift = True + cls.product_2 = cls.env.ref("product.product_product_2") + cls.product_2.is_gift = False + + +class TestGiftContract(TestGiftContractBase): + def _get_gift_sale_order(self): + order = self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "partner_shipping_id": self.partner_gift_to.id, + "order_line": [ + Command.create( + { + "product_id": self.product_1.id, + "date_start": self.today, + } + ), + ], + } + ) + return order + + def _generate_contract_from_order(self): + order = self._get_gift_sale_order() + order.action_confirm() + contract = order.order_line.gift_contract_id[0] + return contract + + def test_check_gift_contract_date(self): + contract = self._generate_contract_from_order() + self.assertEqual(contract.date_start, self.today) + self.assertEqual(contract.recurring_rule_type, "yearly") + self.assertEqual(contract.recurring_interval, 1) + for line in contract.contract_line_ids: + self.assertEqual(line.recurring_rule_type, "yearly") + self.assertEqual(line.recurring_interval, 1) + + def test_gift_contract_partner(self): + contract = self._generate_contract_from_order() + assert contract.partner_id.id != self.partner_gift_to.id + assert contract.partner_id.name == self.partner_gift_to.name + assert contract.partner_id.email == self.partner_gift_to.email + assert contract.partner_id.street == self.partner_gift_to.street + assert contract.partner_id.type == "contact" + + def test_gift_contract_is_gift(self): + contract = self._generate_contract_from_order() + self.assertTrue(contract.is_gift) + + def test_gift_contract_invoice_generation(self): + contract = self._generate_contract_from_order() + contract.cron_recurring_create_invoice(date_ref=self.today) + invoices = self.env["account.move"].search( + [ + ("move_type", "=", "out_invoice"), + ("partner_id", "=", contract.partner_id.id), + ] + ) + self.assertEqual(len(invoices), 1) + self.assertTrue( + any(invoices.line_ids.contract_line_id.contract_id.mapped("is_gift")) + ) + + def test_gift_contract_user_creation(self): + contract = self._generate_contract_from_order() + self.assertFalse(contract.partner_id.user_id) + contract.cron_recurring_create_invoice(date_ref=self.today) + self.assertTrue(contract.partner_id.user_ids) + + def test_find_contact_partners(self): + """Test method `find_contact_partners()`.""" + # existing partners + partner1 = self.env["res.partner"].create( + { + "name": "Test1 Test", + "email": "test@test.tld", + "type": "contact", + "country_id": 1, + "zip": 1000, + } + ) + partner2 = self.env["res.partner"].create( + { + "name": "Test2 Test", + "email": "test@test.tld", + "type": "contact", + "country_id": 1, + "zip": 1000, + } + ) + # new partner with a match + partner_new = self.env["res.partner"].new( + { + "name": "Test2 Test", + "email": "test@test.tld", + "type": "contact", + "country_id": 1, + "zip": 1000, + } + ) + self.assertEqual( + self.env["sale.order"].find_contact_partners(partner_new), + partner2, + ) + # new partner without a match + partner_new = self.env["res.partner"].new( + { + "name": "Test Test", + "email": "test@test.tld", + "type": "contact", + "country_id": 1, + "zip": 2000, + } + ) + self.assertEqual( + self.env["sale.order"].find_contact_partners(partner_new), + partner1 | partner2, + ) + + def test_is_product_compatible_empty(self): + self.assertTrue( + self.env["sale.order"]._is_product_compatible( + self.env["product.product"], + ) + ) + + def test_is_product_compatible_mixed(self): + self.assertFalse( + self.env["sale.order"]._is_product_compatible( + self.product_1 | self.product_2 + ) + ) + + def test_is_product_compatible_only_gift(self): + self.assertTrue(self.env["sale.order"]._is_product_compatible(self.product_1)) + + def test_is_product_compatible_only_non_gift(self): + self.assertTrue(self.env["sale.order"]._is_product_compatible(self.product_2)) diff --git a/website_sale_product_contract_gift/views/product_views.xml b/website_sale_product_contract_gift/views/product_views.xml new file mode 100644 index 000000000..ec8c66e7d --- /dev/null +++ b/website_sale_product_contract_gift/views/product_views.xml @@ -0,0 +1,15 @@ + + + + + product.template.common.is_gift.form + product.template + + + + + + + + + diff --git a/website_sale_product_contract_gift/views/templates.xml b/website_sale_product_contract_gift/views/templates.xml new file mode 100644 index 000000000..26f82df36 --- /dev/null +++ b/website_sale_product_contract_gift/views/templates.xml @@ -0,0 +1,122 @@ + + + + + + + + + + diff --git a/website_sale_restrict_sepa_dd/__init__.py b/website_sale_restrict_sepa_dd/__init__.py index 4ece112b6..7fbcfccdd 100644 --- a/website_sale_restrict_sepa_dd/__init__.py +++ b/website_sale_restrict_sepa_dd/__init__.py @@ -2,4 +2,3 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later from . import models -from . import controllers diff --git a/website_sale_restrict_sepa_dd/__manifest__.py b/website_sale_restrict_sepa_dd/__manifest__.py index cc186a064..9552a25ea 100644 --- a/website_sale_restrict_sepa_dd/__manifest__.py +++ b/website_sale_restrict_sepa_dd/__manifest__.py @@ -15,12 +15,11 @@ "application": False, "depends": [ "payment_sepa_dd", - "website_sale", + "website_sale_product_compatibility", ], "excludes": [], "data": [ "views/product_views.xml", - "views/templates.xml", ], "demo": [], "qweb": [], diff --git a/website_sale_restrict_sepa_dd/models/sale_order.py b/website_sale_restrict_sepa_dd/models/sale_order.py index ab7696e34..3f4c5a153 100644 --- a/website_sale_restrict_sepa_dd/models/sale_order.py +++ b/website_sale_restrict_sepa_dd/models/sale_order.py @@ -48,50 +48,33 @@ def _compute_only_sepa_dd_payment(self): ) def check_product_compatibility(self, product_id): - self.ensure_one() - product = self.env["product.product"].browse(product_id).exists() - warning = "" - only_sepa_dd_product = self.order_line.mapped("product_id").filtered( - lambda p: p.only_sepa_dd_payment - ) - allow_sepa_dd_payment = all( - self.order_line.mapped("product_id").mapped("allow_sepa_dd_payment") - ) - if product: - if only_sepa_dd_product and not product.allow_sepa_dd_payment: - # This product cannot be added - warning = _( - f"Product {product.name} cannot be added to cart " - "because product(s) : " - f"{', '.join(p.name for p in only_sepa_dd_product)} " - "must be payed by SEPA Direct Debit and product " - f"{product.name} cannot be payed by such method." - "Please process this order first, or clear " - f"the order." - ) - elif not allow_sepa_dd_payment and product.only_sepa_dd_payment: - warning = _( - f"Product {product.name} cannot be added to cart " - "because other products in the cart does not allow " - "SEPA Direct Debit as payment method and " - f"{product.name} must be payed with such a method." - "Please process this order first, or clear it." - ) + warning = super().check_product_compatibility(product_id) + if not warning: + product = self.env["product.product"].browse(product_id).exists() + only_sepa_dd_product = self.order_line.mapped("product_id").filtered( + lambda p: p.only_sepa_dd_payment + ) + allow_sepa_dd_payment = all( + self.order_line.mapped("product_id").mapped("allow_sepa_dd_payment") + ) + if product: + if only_sepa_dd_product and not product.allow_sepa_dd_payment: + # This product cannot be added + warning = _( + f"Product {product.name} cannot be added to cart " + "because product(s) : " + f"{', '.join(p.name for p in only_sepa_dd_product)} " + "must be payed by SEPA Direct Debit and product " + f"{product.name} cannot be payed by such method." + "Please process this order first, or clear " + f"the order." + ) + elif not allow_sepa_dd_payment and product.only_sepa_dd_payment: + warning = _( + f"Product {product.name} cannot be added to cart " + "because other products in the cart does not allow " + "SEPA Direct Debit as payment method and " + f"{product.name} must be payed with such a method." + "Please process this order first, or clear it." + ) return warning - - def _cart_update(self, product_id, line_id=None, add_qty=0, set_qty=0, **kwargs): - """Prevent incompatible product to be added to the cart.""" - self.ensure_one() - warning = self.check_product_compatibility(product_id) - if warning: - add_qty = None - set_qty = 0 - values = super()._cart_update( - product_id=product_id, - line_id=line_id, - add_qty=add_qty, - set_qty=set_qty, - **kwargs, - ) - values["warning"] = warning - return values diff --git a/website_sale_restrict_sepa_dd/static/description/index.html b/website_sale_restrict_sepa_dd/static/description/index.html index a2774acac..768f4cafd 100644 --- a/website_sale_restrict_sepa_dd/static/description/index.html +++ b/website_sale_restrict_sepa_dd/static/description/index.html @@ -367,7 +367,7 @@

Restrict SEPA Direct Debit Payment

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:d5514507c207937042ea64484cf364dd8c11232d97806933a51e12dcf7c534f9 +!! source digest: sha256:a25ed6054ea1e7db1fe6f90cdb8102edd80859c7504cc1cebdaae1a0123c4819 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 coopiteasy/addons

Form to order subscription product