diff --git a/sale_partner_primeship/README.rst b/sale_partner_primeship/README.rst new file mode 100644 index 00000000000..dd333641ba8 --- /dev/null +++ b/sale_partner_primeship/README.rst @@ -0,0 +1,139 @@ +====================== +Sale Partner Primeship +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4a8b2bd09f624001cb742f00bfe88a35f7c7ba7dceca566dae0bb4ed0d49e56e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/18.0/sale_partner_primeship + :alt: OCA/sale-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sale_partner_primeship + :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/sale-workflow&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module introduces a membership concept called "primeship" for +partners. + +Primeship functions as a straightforward, user-friendly membership +system that activates automatically when a sale order containing a +primeship activation product is confirmed. The membership duration is +customizable and defined in months. + +The system automatically deactivates the primeship when the specified +duration expires. Users can easily verify a customer's active primeship +status and implement conditional actions based on this membership state. + +Key features: + +- Simple membership management for partners +- Automatic activation through sales orders +- Configurable duration in months +- Automatic expiration handling +- Easy status verification for conditional business logic + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To configure a product that activates primeship upon sale order +confirmation: + +- Edit the desired product and set its type to "Service" +- Navigate to the "Sales" tab in the product form +- Enable the "Activates primeship" checkbox +- Define the primeship duration (in months) + +|image1| + +You can easily view a customer's current primeship status directly from +their partner record: + +|image2| + +Clicking on the primeship status widget will redirect you to a detailed +list view showing all primeship records for that specific customer: + +|image3| + +This interface allows you to track membership history, expiration dates, +and manage customer primeship records efficiently. + +.. |image1| image:: https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_partner_primeship/static/description/primeship-product.png +.. |image2| image:: https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_partner_primeship/static/description/partner-with-primeship.png +.. |image3| image:: https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_partner_primeship/static/description/primeship-partner-view.png + +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 + +Contributors +------------ + +- `Akretion `__: + + - Florian Mounier + - Kevin Roche + - Olivier Nibart + +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-nayatec| image:: https://github.com/nayatec.png?size=40px + :target: https://github.com/nayatec + :alt: nayatec +.. |maintainer-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainers `__: + +|maintainer-nayatec| |maintainer-paradoxxxzero| + +This module is part of the `OCA/sale-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_partner_primeship/__init__.py b/sale_partner_primeship/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_partner_primeship/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_partner_primeship/__manifest__.py b/sale_partner_primeship/__manifest__.py new file mode 100644 index 00000000000..9573a4d2606 --- /dev/null +++ b/sale_partner_primeship/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2021 Akretion - Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Sale Partner Primeship", + "summary": """Allow you to manage time limited prime memberships + and prime membership activation products.""", + "version": "18.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "maintainers": ["nayatec", "paradoxxxzero"], + "website": "https://github.com/OCA/sale-workflow", + "category": "Sales", + "depends": ["sale", "account_invoice_start_end_dates"], + "data": [ + "views/product_template_views.xml", + "views/sale_primeship_views.xml", + "views/res_partner_views.xml", + "security/ir.model.access.csv", + "security/sale_partner_primeship.xml", + "data/ir_cron.xml", + ], + "license": "AGPL-3", +} diff --git a/sale_partner_primeship/data/ir_cron.xml b/sale_partner_primeship/data/ir_cron.xml new file mode 100644 index 00000000000..55c6f221c4e --- /dev/null +++ b/sale_partner_primeship/data/ir_cron.xml @@ -0,0 +1,17 @@ + + + + Check for Expired Partner Primeships + + code + model._check_expired_primeships() + + + 1 + days + + + diff --git a/sale_partner_primeship/models/__init__.py b/sale_partner_primeship/models/__init__.py new file mode 100644 index 00000000000..4f6df38beaa --- /dev/null +++ b/sale_partner_primeship/models/__init__.py @@ -0,0 +1 @@ +from . import product_template, res_partner, sale_primeship, sale_order diff --git a/sale_partner_primeship/models/product_template.py b/sale_partner_primeship/models/product_template.py new file mode 100644 index 00000000000..181e6e56714 --- /dev/null +++ b/sale_partner_primeship/models/product_template.py @@ -0,0 +1,13 @@ +# Copyright 2021 Akretion France (http://www.akretion.com/) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + primeship_activation = fields.Boolean(string="Activates primeship", default=False) + primeship_duration = fields.Integer( + string="Primeship duration (in months)", default=12 + ) diff --git a/sale_partner_primeship/models/res_partner.py b/sale_partner_primeship/models/res_partner.py new file mode 100644 index 00000000000..d0f99f34727 --- /dev/null +++ b/sale_partner_primeship/models/res_partner.py @@ -0,0 +1,47 @@ +# Copyright 2021 Akretion France (http://www.akretion.com/) +# Copyright 2023 Akretion France (http://www.akretion.com/) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + primeship_ids = fields.One2many( + comodel_name="sale.primeship", + inverse_name="partner_id", + required=True, + ) + + # store True allow to filter on active_primeship. + # There is a cron job to set it to False when the customer has no active + # primeship anymore. + active_primeship = fields.Boolean( + compute="_compute_active_primeship", + store=True, + ) + primeship_count = fields.Integer( + string="Primeships Count", compute="_compute_primeship_count" + ) + + @api.depends( + "commercial_partner_id.primeship_ids", + "commercial_partner_id.primeship_ids.current", + ) + def _compute_active_primeship(self): + for record in self: + record.active_primeship = ( + record.commercial_partner_id.primeship_ids.filtered("current") + ) + + @api.depends("commercial_partner_id.primeship_ids") + def _compute_primeship_count(self): + for record in self: + record.primeship_count = len(record.commercial_partner_id.primeship_ids) + + @api.model + def _check_expired_primeships(self): + self.with_context( + active_test=False, + ).search([("active_primeship", "=", True)])._compute_active_primeship() diff --git a/sale_partner_primeship/models/sale_order.py b/sale_partner_primeship/models/sale_order.py new file mode 100644 index 00000000000..0c827d12305 --- /dev/null +++ b/sale_partner_primeship/models/sale_order.py @@ -0,0 +1,111 @@ +# Copyright 2021 Akretion France (http://www.akretion.com/) +# Copyright 2024 Akretion France (http://www.akretion.com/) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _action_confirm(self): + super()._action_confirm() + sale_primeship = self.env["sale.primeship"] + for record in self: + partner = record.partner_id.commercial_partner_id + + for line in record.order_line: + product_template = line.product_template_id + + if product_template.primeship_activation: + # We do not take in account product_qty for now: + duration = product_template.primeship_duration + start = fields.Date.context_today(record) + end = start + relativedelta(months=duration) + + # If we have already some primeships, we need to check for overlaps + if partner.primeship_ids: + # We assume no overlaps between partner primeships + for primeship in partner.primeship_ids.sorted("start_date"): + if primeship.overlaps(start, end): + start = primeship.end_date + end = start + relativedelta(months=duration) + + vals = { + "start_date": start, + "end_date": end, + "partner_id": partner.id, + "order_line_id": line.id, + # this is to reactivate a maybe deactivated existing primeship + "active": True, + } + if line.primeship_id: + # Hm... something seems to have gone wrong here, + # but we handle it nonetheless. + line.primeship_id.write(vals) + else: + # We may have a deactivated primeship because of an order + # cancellation. + primeship = sale_primeship.with_context( + active_test=False, + ).search([("order_line_id", "=", line.id)]) + if primeship: + primeship.write(vals) + else: + sale_primeship.create(vals) + + return True + + def _action_cancel(self): + rv = super()._action_cancel() + for record in self: + record.order_line.mapped("primeship_id").active = False + return rv + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + primeship_id = fields.Many2one( + string="Primeships", + comodel_name="sale.primeship", + compute="_compute_primeship_id", + inverse="_inverse_primeship_id", + ) + + # One2one impl + primeship_ids = fields.One2many( + comodel_name="sale.primeship", inverse_name="order_line_id" + ) + + @api.depends("primeship_ids") + def _compute_primeship_id(self): + for record in self: + record.primeship_id = record.primeship_ids[:1] + + def _inverse_primeship_id(self): + for record in self: + if record.primeship_ids: + primeship = record.env["sale.primeship"].browse( + record.primeship_ids[0].id + ) + primeship.order_line_id = record + + record.primeship_id.order_line_id = record + + def _prepare_invoice_line(self, **optional_values): + """Update invoice start/end dates. + Set invoice start/end dates to primeship start/end dates + In case of multi quantity, this assumes continuous date ranges.""" + self.ensure_one() + res = super()._prepare_invoice_line(**optional_values) + if self.primeship_id: + res.update( + { + "start_date": self.primeship_id.start_date, + "end_date": self.primeship_id.end_date, + } + ) + return res diff --git a/sale_partner_primeship/models/sale_primeship.py b/sale_partner_primeship/models/sale_primeship.py new file mode 100644 index 00000000000..a3ccfb5aede --- /dev/null +++ b/sale_partner_primeship/models/sale_primeship.py @@ -0,0 +1,77 @@ +# Copyright 2021 Akretion France (http://www.akretion.com/) +# Copyright 2024 Akretion France (http://www.akretion.com/) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class SalePrimeship(models.Model): + _name = "sale.primeship" + _description = "Sale Prime Memberships" + + active = fields.Boolean(default=True) + name = fields.Char(compute="_compute_name") + + start_date = fields.Date(required=True) + # End date day is not included in primeship date range + end_date = fields.Date(required=True) + + partner_id = fields.Many2one( + comodel_name="res.partner", + required=True, + ondelete="cascade", + index=True, + ) + order_line_id = fields.Many2one( + comodel_name="sale.order.line", + string="Sale Order Line", + ) + order_id = fields.Many2one(string="Sale Order", related="order_line_id.order_id") + + current = fields.Boolean(string="Currently Active", compute="_compute_current") + + _sql_constraints = [ + # Constraint for One2one impl of "sale.order.line".primeship_id + ( + "unique_order_line", + "UNIQUE(order_line_id)", + "A sale order line can only have one primeship!", + ) + ] + + @api.depends("start_date", "end_date") + def _compute_name(self): + for record in self: + record.name = ( + f"{record.start_date} - {record.end_date} Primeship" + if record + else "New Primeship" + ) + + @api.depends("active", "start_date", "end_date") + def _compute_current(self): + for record in self: + record.current = ( + record.active + and record.start_date + <= fields.Date.context_today(record) + < record.end_date + ) + + @api.constrains("end_date") + def _check_end_date(self): + for record in self: + if record.end_date < record.start_date: + raise ValidationError(_("The end date cannot be before start date")) + + if any( + primeship.overlaps(record.start_date, record.end_date) + for primeship in record.partner_id.primeship_ids + if primeship.id != record.id + ): + raise ValidationError(_("Primeships cannot overlaps")) + + def overlaps(self, start, end): + self.ensure_one() + return self.start_date < end and self.end_date > start diff --git a/sale_partner_primeship/pyproject.toml b/sale_partner_primeship/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/sale_partner_primeship/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/sale_partner_primeship/readme/CONTRIBUTORS.md b/sale_partner_primeship/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..25979a2700c --- /dev/null +++ b/sale_partner_primeship/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- [Akretion](https://www.akretion.com): + - Florian Mounier + - Kevin Roche + - Olivier Nibart diff --git a/sale_partner_primeship/readme/DESCRIPTION.md b/sale_partner_primeship/readme/DESCRIPTION.md new file mode 100644 index 00000000000..c672b92de36 --- /dev/null +++ b/sale_partner_primeship/readme/DESCRIPTION.md @@ -0,0 +1,13 @@ +This module introduces a membership concept called "primeship" for partners. + +Primeship functions as a straightforward, user-friendly membership system that activates automatically when a sale order containing a primeship activation product is confirmed. The membership duration is customizable and defined in months. + +The system automatically deactivates the primeship when the specified duration expires. Users can easily verify a customer's active primeship status and implement conditional actions based on this membership state. + +Key features: + + - Simple membership management for partners + - Automatic activation through sales orders + - Configurable duration in months + - Automatic expiration handling + - Easy status verification for conditional business logic diff --git a/sale_partner_primeship/readme/USAGE.md b/sale_partner_primeship/readme/USAGE.md new file mode 100644 index 00000000000..40e2c12bf2f --- /dev/null +++ b/sale_partner_primeship/readme/USAGE.md @@ -0,0 +1,18 @@ +To configure a product that activates primeship upon sale order confirmation: + + - Edit the desired product and set its type to "Service" + - Navigate to the "Sales" tab in the product form + - Enable the "Activates primeship" checkbox + - Define the primeship duration (in months) + +![](../static/description/primeship-product.png) + +You can easily view a customer's current primeship status directly from their partner record: + +![](../static/description/partner-with-primeship.png) + +Clicking on the primeship status widget will redirect you to a detailed list view showing all primeship records for that specific customer: + +![](../static/description/primeship-partner-view.png) + +This interface allows you to track membership history, expiration dates, and manage customer primeship records efficiently. diff --git a/sale_partner_primeship/security/ir.model.access.csv b/sale_partner_primeship/security/ir.model.access.csv new file mode 100644 index 00000000000..e5fb36d0891 --- /dev/null +++ b/sale_partner_primeship/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_primeship_form,sale.primeship,model_sale_primeship,sales_team.group_sale_manager,1,1,1,1 +access_sale_primeship_form_manager,sale.primeship,model_sale_primeship,account.group_account_readonly,1,0,0,0 diff --git a/sale_partner_primeship/security/sale_partner_primeship.xml b/sale_partner_primeship/security/sale_partner_primeship.xml new file mode 100644 index 00000000000..7c1fd20af8f --- /dev/null +++ b/sale_partner_primeship/security/sale_partner_primeship.xml @@ -0,0 +1,12 @@ + + + + access_sale_partner_primeship_salesman + + + + + + + + diff --git a/sale_partner_primeship/static/description/index.html b/sale_partner_primeship/static/description/index.html new file mode 100644 index 00000000000..1f1e78e5126 --- /dev/null +++ b/sale_partner_primeship/static/description/index.html @@ -0,0 +1,467 @@ + + + + + +Sale Partner Primeship + + + +
+

Sale Partner Primeship

+ + +

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

+

This module introduces a membership concept called “primeship” for +partners.

+

Primeship functions as a straightforward, user-friendly membership +system that activates automatically when a sale order containing a +primeship activation product is confirmed. The membership duration is +customizable and defined in months.

+

The system automatically deactivates the primeship when the specified +duration expires. Users can easily verify a customer’s active primeship +status and implement conditional actions based on this membership state.

+

Key features:

+
    +
  • Simple membership management for partners
  • +
  • Automatic activation through sales orders
  • +
  • Configurable duration in months
  • +
  • Automatic expiration handling
  • +
  • Easy status verification for conditional business logic
  • +
+

Table of contents

+ +
+

Usage

+

To configure a product that activates primeship upon sale order +confirmation:

+
    +
  • Edit the desired product and set its type to “Service”
  • +
  • Navigate to the “Sales” tab in the product form
  • +
  • Enable the “Activates primeship” checkbox
  • +
  • Define the primeship duration (in months)
  • +
+

image1

+

You can easily view a customer’s current primeship status directly from +their partner record:

+

image2

+

Clicking on the primeship status widget will redirect you to a detailed +list view showing all primeship records for that specific customer:

+

image3

+

This interface allows you to track membership history, expiration dates, +and manage customer primeship records efficiently.

+
+
+

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
  • +
+
+
+

Contributors

+
    +
  • Akretion:
      +
    • Florian Mounier
    • +
    • Kevin Roche
    • +
    • Olivier Nibart
    • +
    +
  • +
+
+
+

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 maintainers:

+

nayatec paradoxxxzero

+

This module is part of the OCA/sale-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/sale_partner_primeship/static/description/partner-with-primeship.png b/sale_partner_primeship/static/description/partner-with-primeship.png new file mode 100644 index 00000000000..9e30f30345f Binary files /dev/null and b/sale_partner_primeship/static/description/partner-with-primeship.png differ diff --git a/sale_partner_primeship/static/description/primeship-partner-view.png b/sale_partner_primeship/static/description/primeship-partner-view.png new file mode 100644 index 00000000000..943eb458d4b Binary files /dev/null and b/sale_partner_primeship/static/description/primeship-partner-view.png differ diff --git a/sale_partner_primeship/static/description/primeship-product.png b/sale_partner_primeship/static/description/primeship-product.png new file mode 100644 index 00000000000..16dfea8c119 Binary files /dev/null and b/sale_partner_primeship/static/description/primeship-product.png differ diff --git a/sale_partner_primeship/tests/__init__.py b/sale_partner_primeship/tests/__init__.py new file mode 100644 index 00000000000..934f37b2331 --- /dev/null +++ b/sale_partner_primeship/tests/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2024 Akretion - Olivier Nibart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_res_partner_primeship +from . import test_sale_order_primeship +from . import test_sale_primeship diff --git a/sale_partner_primeship/tests/common.py b/sale_partner_primeship/tests/common.py new file mode 100644 index 00000000000..d49a9fe8a10 --- /dev/null +++ b/sale_partner_primeship/tests/common.py @@ -0,0 +1,48 @@ +# Copyright 2024 Akretion - Olivier Nibart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.base.tests.common import BaseCommon + + +class TestCommon(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.SalePrimeship = cls.env["sale.primeship"] + cls.SaleOrderLine = cls.env["sale.order.line"] + cls.Partner = cls.env["res.partner"] + cls.product = cls.env["product.template"].create( + { + "name": "Primeship Product", + "primeship_activation": True, + "primeship_duration": 6, + } + ) + cls.order = ( + cls.env["sale.order"] + .with_context(disable_cancel_warning=True) + .create( + { + "partner_id": cls.partner.id, + } + ) + ) + + def make_primeship(self, start_date, end_date, order_line_id=None): + return self.SalePrimeship.create( + { + "partner_id": self.partner.id, + "start_date": start_date, + "end_date": end_date, + "order_line_id": order_line_id.id if order_line_id else False, + } + ) + + def make_order_line(self): + return self.SaleOrderLine.create( + { + "order_id": self.order.id, + "product_id": self.product.product_variant_id.id, + "product_uom_qty": 1, + } + ) diff --git a/sale_partner_primeship/tests/test_res_partner_primeship.py b/sale_partner_primeship/tests/test_res_partner_primeship.py new file mode 100644 index 00000000000..2d74f82976e --- /dev/null +++ b/sale_partner_primeship/tests/test_res_partner_primeship.py @@ -0,0 +1,61 @@ +# Copyright 2024 Akretion - Olivier Nibart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import date, timedelta + +from .common import TestCommon + + +class TestResPartnerPrimeship(TestCommon): + def test_compute_active_primeship(self): + active_primeship = self.make_primeship( + date.today() - timedelta(days=1), date.today() + timedelta(days=30) + ) + self.assertTrue(active_primeship.current) + self.partner._compute_active_primeship() + self.assertTrue(self.partner.active_primeship) + + expired_primeship = self.make_primeship( + date.today() - timedelta(days=60), date.today() - timedelta(days=30) + ) + self.assertFalse(expired_primeship.current) + self.partner._compute_active_primeship() + # Still true due to active primeship + self.assertTrue(self.partner.active_primeship) + + active_primeship.active = False + self.partner._compute_active_primeship() + self.assertFalse(self.partner.active_primeship) + + def test_compute_primeship_count(self): + self.assertEqual(self.partner.primeship_count, 0) + + self.make_primeship(date.today(), date.today() + timedelta(days=30)) + self.partner._compute_primeship_count() + self.assertEqual(self.partner.primeship_count, 1) + + self.make_primeship( + date.today() + timedelta(days=31), date.today() + timedelta(days=60) + ) + self.partner._compute_primeship_count() + self.assertEqual(self.partner.primeship_count, 2) + + def test_check_expired_primeships(self): + primeship = self.make_primeship( + date.today() - timedelta(days=1), date.today() + timedelta(days=30) + ) + self.assertTrue(self.partner.active_primeship) + self.Partner._check_expired_primeships() + self.assertTrue(self.partner.active_primeship) + + primeship.start_date = date.today() + timedelta(days=1) + self.assertFalse(self.partner.active_primeship) + self.partner.active_primeship = True + self.Partner._check_expired_primeships() + self.assertFalse(self.partner.active_primeship) + + # work also on inactive partners + self.partner.active = False + self.partner.active_primeship = True + self.Partner._check_expired_primeships() + self.assertFalse(self.partner.active_primeship) diff --git a/sale_partner_primeship/tests/test_sale_order_primeship.py b/sale_partner_primeship/tests/test_sale_order_primeship.py new file mode 100644 index 00000000000..f6aba4639f2 --- /dev/null +++ b/sale_partner_primeship/tests/test_sale_order_primeship.py @@ -0,0 +1,94 @@ +# Copyright 2024 Akretion - Olivier Nibart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import date + +from dateutil.relativedelta import relativedelta + +from .common import TestCommon + + +class TestSaleOrderPrimeship(TestCommon): + def test_action_confirm_creates_primeship(self): + order_line = self.make_order_line() + self.order.action_confirm() + + self.assertTrue(order_line.primeship_id) + self.assertEqual(order_line.primeship_id.partner_id, self.partner) + self.assertEqual(order_line.primeship_id.order_line_id, order_line) + self.assertEqual(order_line.primeship_id.start_date, date.today()) + self.assertEqual( + order_line.primeship_id.end_date, + order_line.primeship_id.start_date + relativedelta(months=6), + ) + + def test_action_confirm_extends_existing_primeship(self): + existing_primeship = self.make_primeship( + date.today() - relativedelta(months=3), + date.today() + relativedelta(months=3), + ) + order_line = self.make_order_line() + self.order.action_confirm() + + self.assertEqual( + order_line.primeship_id.start_date, existing_primeship.end_date + ) + self.assertEqual( + order_line.primeship_id.end_date, + order_line.primeship_id.start_date + relativedelta(months=6), + ) + + def test_action_cancel_deactivates_primeship(self): + order_line = self.make_order_line() + self.order.action_confirm() + original_primeship = order_line.primeship_id + self.assertTrue(original_primeship.active) + + self.order.action_cancel() + self.assertFalse(original_primeship.active) + + def test_action_confirm_with_existing_primeship(self): + order_line = self.make_order_line() + self.order.action_confirm() + original_primeship = order_line.primeship_id + self.order.action_cancel() + + # recomputing the primeship_id will detach + # the deactivated primeship from the order line + order_line._compute_primeship_id() + self.assertFalse(order_line.primeship_id) + + self.order.action_draft() + self.assertFalse(original_primeship.active) + + # we should now retrieve the deactivated primeship + # and reactivate it + self.order.action_confirm() + self.assertTrue(order_line.primeship_id.active) + self.assertEqual(order_line.primeship_id.id, original_primeship.id) + + self.order.action_cancel() + + # we do not recompute the primeship_id so even though it is + # deactivated, order_line.primeship_id is still pointing at it + self.assertEqual(order_line.primeship_id.id, original_primeship.id) + + self.order.action_draft() + + # we should now use the still attached deactivated primeship + # and reactivate it + self.order.action_confirm() + self.assertTrue(original_primeship.active) + self.assertEqual(order_line.primeship_id.id, original_primeship.id) + + def test_prepare_invoice_line_with_primeship(self): + order_line = self.make_order_line() + self.order.action_confirm() + + invoice_line_vals = order_line._prepare_invoice_line() + self.assertEqual( + invoice_line_vals["start_date"], order_line.primeship_id.start_date + ) + self.assertEqual( + invoice_line_vals["end_date"], order_line.primeship_id.end_date + ) diff --git a/sale_partner_primeship/tests/test_sale_primeship.py b/sale_partner_primeship/tests/test_sale_primeship.py new file mode 100644 index 00000000000..9f7de4eee22 --- /dev/null +++ b/sale_partner_primeship/tests/test_sale_primeship.py @@ -0,0 +1,85 @@ +# Copyright 2024 Akretion - Olivier Nibart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import date, timedelta + +import psycopg2 + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tools.misc import mute_logger + +from .common import TestCommon + + +class TestSalePrimeship(TestCommon): + def test_compute_name(self): + primeship = self.make_primeship("2024-01-01", "2024-12-31") + self.assertEqual(primeship.name, "2024-01-01 - 2024-12-31 Primeship") + + def test_compute_current(self): + today = date.today() + primeship_current = self.make_primeship( + today - timedelta(days=1), today + timedelta(days=1) + ) + primeship_past = self.make_primeship( + today - timedelta(days=10), today - timedelta(days=1) + ) + primeship_future = self.make_primeship( + today + timedelta(days=1), today + timedelta(days=10) + ) + self.assertTrue(primeship_current.current) + self.assertFalse(primeship_past.current) + self.assertFalse(primeship_future.current) + + def test_check_end_date(self): + with self.assertRaises(ValidationError): + self.make_primeship("2023-12-31", "2023-01-01") + + def test_overlapping_primeships(self): + self.make_primeship("2023-01-01", "2023-06-30") + with self.assertRaises(ValidationError): + self.make_primeship("2023-06-01", "2023-12-31") + + def test_non_overlapping_primeships(self): + primeship1 = self.make_primeship("2023-01-01", "2023-06-30") + primeship2 = self.make_primeship("2023-07-01", "2023-12-31") + self.assertTrue(primeship1.id) + self.assertTrue(primeship2.id) + + def test_overlaps_method(self): + primeship = self.make_primeship("2023-01-01", "2023-12-31") + self.assertTrue( + primeship.overlaps( + fields.Date.to_date("2023-06-01"), fields.Date.to_date("2023-07-01") + ) + ) + self.assertTrue( + primeship.overlaps( + fields.Date.to_date("2022-06-01"), fields.Date.to_date("2023-02-01") + ) + ) + self.assertTrue( + primeship.overlaps( + fields.Date.to_date("2023-12-01"), fields.Date.to_date("2024-01-01") + ) + ) + self.assertFalse( + primeship.overlaps( + fields.Date.to_date("2022-01-01"), fields.Date.to_date("2022-12-31") + ) + ) + self.assertFalse( + primeship.overlaps( + fields.Date.to_date("2024-01-01"), fields.Date.to_date("2024-12-31") + ) + ) + + def test_sale_order_line_sale_primeship_o2o_relation_sql_constraint(self): + order_line = self.make_order_line() + self.make_primeship("2023-01-01", "2023-12-31", order_line_id=order_line) + with self.assertRaises(psycopg2.IntegrityError): + with mute_logger("odoo.sql_db"), self.cr.savepoint(): + self.make_primeship( + "2024-01-01", "2024-12-31", order_line_id=order_line + ) diff --git a/sale_partner_primeship/views/product_template_views.xml b/sale_partner_primeship/views/product_template_views.xml new file mode 100644 index 00000000000..c5acdc97eaa --- /dev/null +++ b/sale_partner_primeship/views/product_template_views.xml @@ -0,0 +1,23 @@ + + + + product.template.product.form + product.template + + + + + + + + + + + diff --git a/sale_partner_primeship/views/res_partner_views.xml b/sale_partner_primeship/views/res_partner_views.xml new file mode 100644 index 00000000000..e16409e6a90 --- /dev/null +++ b/sale_partner_primeship/views/res_partner_views.xml @@ -0,0 +1,37 @@ + + + + res.partner.form + res.partner + + + + + + + + + + + + diff --git a/sale_partner_primeship/views/sale_primeship_views.xml b/sale_partner_primeship/views/sale_primeship_views.xml new file mode 100644 index 00000000000..d357467c193 --- /dev/null +++ b/sale_partner_primeship/views/sale_primeship_views.xml @@ -0,0 +1,56 @@ + + + + sale.primeship.search + sale.primeship + + + + + + + + + + + + Primeship + sale.primeship + list,form + + + + sale.primeship.form + sale.primeship + +
+ +
+

+ +  Primeship +

+
+ + + + + +
+
+
+
+ + + sale.primeship.list + sale.primeship + + + + + + + + + +