Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

143
sale_partner_primeship/README.rst Executable file
View File

@@ -0,0 +1,143 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
======================
Sale Partner Primeship
======================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:c64dcb81e1aace4fad9c32fcf038dbadb4eaf1129e588e13ee08809be91830da
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/license-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 <https://github.com/OCA/sale-workflow/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 <https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_partner_primeship%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Akretion
Contributors
------------
- `Akretion <https://www.akretion.com>`__:
- 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 <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-nayatec| |maintainer-paradoxxxzero|
This module is part of the `OCA/sale-workflow <https://github.com/OCA/sale-workflow/tree/18.0/sale_partner_primeship>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -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",
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record forcecreate="True" id="ir_cron_check_primeship_expired" model="ir.cron">
<field name="name">Check for Expired Partner Primeships</field>
<field name="model_id" ref="sale_partner_primeship.model_res_partner" />
<field name="state">code</field>
<field name="code">model._check_expired_primeships()</field>
<field name="active" eval="True" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field
name="nextcall"
eval="(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
</odoo>

180
sale_partner_primeship/i18n/it.po Executable file
View File

@@ -0,0 +1,180 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_partner_primeship
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-09-29 17:43+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.4\n"
#. module: sale_partner_primeship
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.view_partner_form
msgid "<span class=\"o_stat_text\">Primeships</span>"
msgstr "<span class=\"o_stat_text\">Spedizione prioritaria</span>"
#. module: sale_partner_primeship
#: model:ir.model.constraint,message:sale_partner_primeship.constraint_sale_primeship_unique_order_line
msgid "A sale order line can only have one primeship!"
msgstr "Una riga ordine vendita può avere una sola spedizione prioritaria!"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_product_product__primeship_activation
#: model:ir.model.fields,field_description:sale_partner_primeship.field_product_template__primeship_activation
msgid "Activates primeship"
msgstr "Attiva spedizione prioritaria"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__active
msgid "Active"
msgstr "Attiva"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_partner__active_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_users__active_primeship
msgid "Active Primeship"
msgstr "Attiva spedizione prioritaria"
#. module: sale_partner_primeship
#: model:ir.actions.server,name:sale_partner_primeship.ir_cron_check_primeship_expired_ir_actions_server
msgid "Check for Expired Partner Primeships"
msgstr "Controllo spedizioni prioritarie partner scadute"
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_res_partner
msgid "Contact"
msgstr "Contatto"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__create_uid
msgid "Created by"
msgstr "Creato da"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__create_date
msgid "Created on"
msgstr "Creato il"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__current
msgid "Currently Active"
msgstr "Attualmente attiva"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__end_date
msgid "End Date"
msgstr "Data fine"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__id
msgid "ID"
msgstr "ID"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__write_uid
msgid "Last Updated by"
msgstr "Ultimo aggiornamento di"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__write_date
msgid "Last Updated on"
msgstr "Ultimo aggiornamento il"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__name
msgid "Name"
msgstr "Nome"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__partner_id
msgid "Partner"
msgstr "Partner"
#. module: sale_partner_primeship
#: model:ir.actions.act_window,name:sale_partner_primeship.sale_primeship_action
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_partner__primeship_ids
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_users__primeship_ids
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_order_line__primeship_ids
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.sale_primeship_view_form
msgid "Primeship"
msgstr "Spedizione prioritaria"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_product_product__primeship_duration
#: model:ir.model.fields,field_description:sale_partner_primeship.field_product_template__primeship_duration
msgid "Primeship duration (in months)"
msgstr "Durata spedizione prioritaria (in mesi)"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_order_line__primeship_id
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.product_template_only_form_view
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.sale_primeship_view_tree
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.view_partner_form
msgid "Primeships"
msgstr "Spedizioni prioritarie"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_partner__primeship_count
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_users__primeship_count
msgid "Primeships Count"
msgstr "Conteggio spedizioni prioritarie"
#. module: sale_partner_primeship
#. odoo-python
#: code:addons/sale_partner_primeship/models/sale_primeship.py:0
msgid "Primeships cannot overlaps"
msgstr "Le spedizioni prioritarie non possono sovrapporsi"
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_product_template
msgid "Product"
msgstr "Prodotto"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__order_id
msgid "Sale Order"
msgstr "Ordine di vendita"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__order_line_id
msgid "Sale Order Line"
msgstr "Riga ordine di vendita"
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_sale_primeship
msgid "Sale Prime Memberships"
msgstr "Iscrizione vendita prioritaria"
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_sale_order
msgid "Sales Order"
msgstr "Ordine di vendita"
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_sale_order_line
msgid "Sales Order Line"
msgstr "Riga ordine di vendita"
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__start_date
msgid "Start Date"
msgstr "Data inizio"
#. module: sale_partner_primeship
#. odoo-python
#: code:addons/sale_partner_primeship/models/sale_primeship.py:0
msgid "The end date cannot be before start date"
msgstr "La data fine non può essere antecedente alla data inizio"

View File

@@ -0,0 +1,177 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_partner_primeship
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: sale_partner_primeship
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.view_partner_form
msgid "<span class=\"o_stat_text\">Primeships</span>"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.constraint,message:sale_partner_primeship.constraint_sale_primeship_unique_order_line
msgid "A sale order line can only have one primeship!"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_product_product__primeship_activation
#: model:ir.model.fields,field_description:sale_partner_primeship.field_product_template__primeship_activation
msgid "Activates primeship"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__active
msgid "Active"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_partner__active_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_users__active_primeship
msgid "Active Primeship"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.actions.server,name:sale_partner_primeship.ir_cron_check_primeship_expired_ir_actions_server
msgid "Check for Expired Partner Primeships"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_res_partner
msgid "Contact"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__create_uid
msgid "Created by"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__create_date
msgid "Created on"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__current
msgid "Currently Active"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__display_name
msgid "Display Name"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__end_date
msgid "End Date"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__id
msgid "ID"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__write_uid
msgid "Last Updated by"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__write_date
msgid "Last Updated on"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__name
msgid "Name"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__partner_id
msgid "Partner"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.actions.act_window,name:sale_partner_primeship.sale_primeship_action
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_partner__primeship_ids
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_users__primeship_ids
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_order_line__primeship_ids
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.sale_primeship_view_form
msgid "Primeship"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_product_product__primeship_duration
#: model:ir.model.fields,field_description:sale_partner_primeship.field_product_template__primeship_duration
msgid "Primeship duration (in months)"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_order_line__primeship_id
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.product_template_only_form_view
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.sale_primeship_view_tree
#: model_terms:ir.ui.view,arch_db:sale_partner_primeship.view_partner_form
msgid "Primeships"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_partner__primeship_count
#: model:ir.model.fields,field_description:sale_partner_primeship.field_res_users__primeship_count
msgid "Primeships Count"
msgstr ""
#. module: sale_partner_primeship
#. odoo-python
#: code:addons/sale_partner_primeship/models/sale_primeship.py:0
msgid "Primeships cannot overlaps"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_product_template
msgid "Product"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__order_id
msgid "Sale Order"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__order_line_id
msgid "Sale Order Line"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_sale_primeship
msgid "Sale Prime Memberships"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_sale_order
msgid "Sales Order"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model,name:sale_partner_primeship.model_sale_order_line
msgid "Sales Order Line"
msgstr ""
#. module: sale_partner_primeship
#: model:ir.model.fields,field_description:sale_partner_primeship.field_sale_primeship__start_date
msgid "Start Date"
msgstr ""
#. module: sale_partner_primeship
#. odoo-python
#: code:addons/sale_partner_primeship/models/sale_primeship.py:0
msgid "The end date cannot be before start date"
msgstr ""

View File

@@ -0,0 +1 @@
from . import product_template, res_partner, sale_primeship, sale_order

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,4 @@
- [Akretion](https://www.akretion.com):
- Florian Mounier
- Kevin Roche
- Olivier Nibart

View File

@@ -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

View File

@@ -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.

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sale_primeship_form sale.primeship model_sale_primeship sales_team.group_sale_manager 1 1 1 1
3 access_sale_primeship_form_manager sale.primeship model_sale_primeship account.group_account_readonly 1 0 0 0

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.model.access" id="access_sale_partner_primeship_salesman">
<field name="name">access_sale_partner_primeship_salesman</field>
<field name="model_id" ref="sale_partner_primeship.model_sale_primeship" />
<field name="group_id" ref="sales_team.group_sale_salesman" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="1" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,473 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="sale-partner-primeship">
<h1>Sale Partner Primeship</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:c64dcb81e1aace4fad9c32fcf038dbadb4eaf1129e588e13ee08809be91830da
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/sale-workflow/tree/18.0/sale_partner_primeship"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sale_partner_primeship"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module introduces a membership concept called “primeship” for
partners.</p>
<p>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.</p>
<p>The system automatically deactivates the primeship when the specified
duration expires. Users can easily verify a customers active primeship
status and implement conditional actions based on this membership state.</p>
<p>Key features:</p>
<ul class="simple">
<li>Simple membership management for partners</li>
<li>Automatic activation through sales orders</li>
<li>Configurable duration in months</li>
<li>Automatic expiration handling</li>
<li>Easy status verification for conditional business logic</li>
</ul>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<p>To configure a product that activates primeship upon sale order
confirmation:</p>
<ul class="simple">
<li>Edit the desired product and set its type to “Service”</li>
<li>Navigate to the “Sales” tab in the product form</li>
<li>Enable the “Activates primeship” checkbox</li>
<li>Define the primeship duration (in months)</li>
</ul>
<p><img alt="image1" src="https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_partner_primeship/static/description/primeship-product.png" /></p>
<p>You can easily view a customers current primeship status directly from
their partner record:</p>
<p><img alt="image2" src="https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_partner_primeship/static/description/partner-with-primeship.png" /></p>
<p>Clicking on the primeship status widget will redirect you to a detailed
list view showing all primeship records for that specific customer:</p>
<p><img alt="image3" src="https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_partner_primeship/static/description/primeship-partner-view.png" /></p>
<p>This interface allows you to track membership history, expiration dates,
and manage customer primeship records efficiently.</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/sale-workflow/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_partner_primeship%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-3">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
<ul class="simple">
<li>Akretion</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
<ul class="simple">
<li><a class="reference external" href="https://www.akretion.com">Akretion</a>:<ul>
<li>Florian Mounier</li>
<li>Kevin Roche</li>
<li>Olivier Nibart</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/nayatec"><img alt="nayatec" src="https://github.com/nayatec.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/paradoxxxzero"><img alt="paradoxxxzero" src="https://github.com/paradoxxxzero.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/sale-workflow/tree/18.0/sale_partner_primeship">OCA/sale-workflow</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -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

View File

@@ -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,
}
)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="product_template_only_form_view" model="ir.ui.view">
<field name="name">product.template.product.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view" />
<field name="arch" type="xml">
<xpath expr="//page[@name='sales']" position="inside">
<group
string="Primeships"
name="primeships"
invisible="type != 'service'"
>
<field name="primeship_activation" />
<field
name="primeship_duration"
invisible="not primeship_activation"
/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">res.partner.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<field name="commercial_partner_id" invisible="1" />
<button
class="oe_stat_button"
name="%(sale_primeship_action)d"
type="action"
groups="sales_team.group_sale_manager"
icon="fa-id-card"
context="{'search_default_partner_id': commercial_partner_id, 'default_partner_id': commercial_partner_id}"
>
<div class="o_form_field o_stat_info">
<div class="o_row">
<span class="o_stat_value">
<field
string="Primeships"
name="primeship_count"
widget="integer"
/>
</span>
</div>
<span class="o_stat_text">Primeships</span>
</div>
</button>
</xpath>
<xpath expr="//field[@name='vat']" position="after">
<field name="active_primeship" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="sale_primeship_search" model="ir.ui.view">
<field name="name">sale.primeship.search</field>
<field name="model">sale.primeship</field>
<field name="arch" type="xml">
<search>
<field name="start_date" />
<field name="end_date" />
<field name="partner_id" />
<field name="current" />
</search>
</field>
</record>
<record id="sale_primeship_action" model="ir.actions.act_window">
<field name="name">Primeship</field>
<field name="res_model">sale.primeship</field>
<field name="view_mode">list,form</field>
</record>
<record id="sale_primeship_view_form" model="ir.ui.view">
<field name="name">sale.primeship.form</field>
<field name="model">sale.primeship</field>
<field name="arch" type="xml">
<form string="Primeship">
<sheet>
<div class="oe_title">
<h1>
<field name="partner_id" readonly="1" />
 Primeship
</h1>
</div>
<group>
<field name="start_date" />
<field name="end_date" />
<field name="order_id" readonly="1" />
</group>
</sheet>
</form>
</field>
</record>
<record id="sale_primeship_view_tree" model="ir.ui.view">
<field name="name">sale.primeship.list</field>
<field name="model">sale.primeship</field>
<field name="arch" type="xml">
<list string="Primeships">
<field name="start_date" />
<field name="end_date" />
<field name="order_id" />
<field name="current" />
</list>
</field>
</record>
</odoo>