Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions product_odoo_module/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Product Odoo Module

This addon tracks Odoo modules you maintain as products and registers which customers
subscribe to each module variant with a monthly maintenance fee.

## Usage

1. Create a module product template:

- Go to Sales > Products > Odoo Modules.
- Create a product and enable **Is Odoo Module**.
- Fill in the technical name and repository URL as needed.
- Tip: module products are typically **Service** products.

2. Create variants by Odoo Series (when needed):

- On the product template, add an attribute line for **Odoo Series**.
- Select the required values (14, 15, 16, 17) to generate variants.
- Variants are created manually by adding the attribute line; the addon does not
auto-assign it.

3. Register customer subscriptions:
- Go to Sales > Products > Customer Modules.
- Create a line with Customer, Module variant, monthly fee, and date range.

Smart buttons:

- **Modules** on a customer shows active module lines for that customer.
- **Customers** on a module template shows active customer lines for its variants.
1 change: 1 addition & 0 deletions product_odoo_module/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
34 changes: 34 additions & 0 deletions product_odoo_module/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2026 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Product Odoo Module",
"category": "Productivity",
"license": "AGPL-3",
"author": "Quartile",
"website": "https://www.quartile.co",
"version": "16.0.1.0.0",
"depends": ["product"],
"data": [
"security/security.xml",
"security/ir.model.access.csv",
"data/module_business_impact_data.xml",
"data/module_complexity_data.xml",
"data/module_license_data.xml",
"data/module_popularity_data.xml",
"data/module_source_type_data.xml",
"data/product_attribute_data.xml",
"views/customer_module_info_views.xml",
"views/module_business_impact_views.xml",
"views/module_complexity_views.xml",
"views/module_license_views.xml",
"views/module_popularity_views.xml",
"views/module_source_type_views.xml",
"views/product_attribute_views.xml",
"views/product_odoo_module_menuitems.xml",
"views/product_product_views.xml",
"views/product_template_views.xml",
"views/res_company_views.xml",
"views/res_partner_views.xml",
],
"installable": True,
}
18 changes: 18 additions & 0 deletions product_odoo_module/data/module_business_impact_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="module_business_impact_tier_a" model="module.business.impact">
<field name="name">Tier-A</field>
<field name="description">Mission Critical</field>
<field name="sequence">10</field>
</record>
<record id="module_business_impact_tier_b" model="module.business.impact">
<field name="name">Tier-B</field>
<field name="description">Major</field>
<field name="sequence">20</field>
</record>
<record id="module_business_impact_tier_c" model="module.business.impact">
<field name="name">Tier-C</field>
<field name="description">Minor</field>
<field name="sequence">30</field>
</record>
</odoo>
18 changes: 18 additions & 0 deletions product_odoo_module/data/module_complexity_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="module_complexity_high" model="module.complexity">
<field name="name">High</field>
<field name="sequence">1</field>
<field name="factor">1.2</field>
</record>
<record id="module_complexity_medium" model="module.complexity">
<field name="name">Medium</field>
<field name="sequence">2</field>
<field name="factor">1.0</field>
</record>
<record id="module_complexity_low" model="module.complexity">
<field name="name">Low</field>
<field name="sequence">3</field>
<field name="factor">0.8</field>
</record>
</odoo>
23 changes: 23 additions & 0 deletions product_odoo_module/data/module_license_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="odoo_module_license_agpl3" model="module.license">
<field name="name">AGPL-3</field>
<field name="sequence">10</field>
<field name="factor">0.8</field>
</record>
<record id="odoo_module_license_lgpl3" model="module.license">
<field name="name">LGPL-3</field>
<field name="sequence">20</field>
<field name="factor">0.8</field>
</record>
<record id="odoo_module_license_gpl3" model="module.license">
<field name="name">GPL-3</field>
<field name="sequence">30</field>
<field name="factor">0.8</field>
</record>
<record id="odoo_module_license_proprietary" model="module.license">
<field name="name">Other Proprietary</field>
<field name="sequence">40</field>
<field name="factor">1.0</field>
</record>
</odoo>
28 changes: 28 additions & 0 deletions product_odoo_module/data/module_popularity_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="module_popularity_very_high" model="module.popularity">
<field name="name">Very High</field>
<field name="sequence">1</field>
<field name="factor">0.2</field>
</record>
<record id="module_popularity_high" model="module.popularity">
<field name="name">High</field>
<field name="sequence">2</field>
<field name="factor">0.4</field>
</record>
<record id="module_popularity_medium" model="module.popularity">
<field name="name">Medium</field>
<field name="sequence">3</field>
<field name="factor">0.6</field>
</record>
<record id="module_popularity_low" model="module.popularity">
<field name="name">Low</field>
<field name="sequence">4</field>
<field name="factor">0.8</field>
</record>
<record id="module_popularity_very_low" model="module.popularity">
<field name="name">Very Low</field>
<field name="sequence">5</field>
<field name="factor">1.0</field>
</record>
</odoo>
23 changes: 23 additions & 0 deletions product_odoo_module/data/module_source_type_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="module_source_type_oca" model="module.source.type">
<field name="name">OCA</field>
<field name="sequence">10</field>
<field name="factor">0.3</field>
</record>
<record id="module_source_type_our_team" model="module.source.type">
<field name="name">Our Team</field>
<field name="sequence">20</field>
<field name="factor">0.5</field>
</record>
<record id="module_source_type_user_in_house" model="module.source.type">
<field name="name">User In-house</field>
<field name="sequence">30</field>
<field name="factor">0.5</field>
</record>
<record id="module_source_type_third_party" model="module.source.type">
<field name="name">Third Party</field>
<field name="sequence">40</field>
<field name="factor">1.0</field>
</record>
</odoo>
48 changes: 48 additions & 0 deletions product_odoo_module/data/product_attribute_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="product_attribute_odoo_series" model="product.attribute">
<field name="name">Series</field>
<field name="create_variant">always</field>
<field name="is_odoo_series" eval="True" />
</record>
<record id="product_attribute_value_odoo_series_10" model="product.attribute.value">
<field name="name">10.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
<record id="product_attribute_value_odoo_series_11" model="product.attribute.value">
<field name="name">11.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
<record id="product_attribute_value_odoo_series_12" model="product.attribute.value">
<field name="name">12.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
<record id="product_attribute_value_odoo_series_13" model="product.attribute.value">
<field name="name">13.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
<record id="product_attribute_value_odoo_series_14" model="product.attribute.value">
<field name="name">14.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
<record id="product_attribute_value_odoo_series_15" model="product.attribute.value">
<field name="name">15.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
<record id="product_attribute_value_odoo_series_16" model="product.attribute.value">
<field name="name">16.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
<record id="product_attribute_value_odoo_series_17" model="product.attribute.value">
<field name="name">17.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
<record id="product_attribute_value_odoo_series_18" model="product.attribute.value">
<field name="name">18.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
<record id="product_attribute_value_odoo_series_19" model="product.attribute.value">
<field name="name">19.0</field>
<field name="attribute_id" ref="product_attribute_odoo_series" />
</record>
</odoo>
11 changes: 11 additions & 0 deletions product_odoo_module/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from . import customer_module_info
from . import module_business_impact
from . import module_complexity
from . import module_license
from . import module_popularity
from . import module_source_type
from . import product_attribute
from . import product_product
from . import product_template
from . import res_company
from . import res_partner
156 changes: 156 additions & 0 deletions product_odoo_module/models/customer_module_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Copyright 2026 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class CustomerModuleInfo(models.Model):
_name = "customer.module.info"
_description = "Customer Module Information"
_order = "partner_id, product_id, date_start desc"

name = fields.Char(compute="_compute_name")
partner_id = fields.Many2one(
"res.partner",
required=True,
ondelete="cascade",
)
product_id = fields.Many2one(
"product.product",
domain=[("is_odoo_module", "=", True)],
required=True,
ondelete="restrict",
)
product_tmpl_id = fields.Many2one(
related="product_id.product_tmpl_id",
store=True,
)
source_type_id = fields.Many2one(
related="product_id.source_type_id",
store=True,
)
module_license_id = fields.Many2one(
related="product_id.module_license_id",
store=True,
)
module_complexity_id = fields.Many2one(
related="product_id.module_complexity_id",
store=True,
)
customer_count = fields.Integer(
related="product_tmpl_id.customer_module_count",
string="Total Customers",
)
fee_factor = fields.Float(
default=1.0,
help="Multiplier applied to the product's suggested fee.",
)
suggested_fee = fields.Monetary(
compute="_compute_suggested_fee",
currency_field="currency_id",
)
loc = fields.Integer(
string="Lines of Code",
related="product_id.loc",
store=True,
readonly=True,
)
company_id = fields.Many2one(
"res.company",
required=True,
default=lambda self: self.env.company,
)
currency_id = fields.Many2one(
related="company_id.currency_id",
store=True,
readonly=True,
)
monthly_fee = fields.Monetary(required=True)
date_start = fields.Date(default=fields.Date.context_today)
date_end = fields.Date()
active = fields.Boolean(default=True)
note = fields.Text()
business_impact_id = fields.Many2one(
"module.business.impact",
compute="_compute_business_impact_id",
store=True,
)
series_value_id = fields.Many2one(
"product.attribute.value",
compute="_compute_series_value_id",
store=True,
)

@api.depends("product_id", "partner_id")
def _compute_name(self):
for record in self:
record.name = f"{record.product_id.name} - {record.partner_id.ref}"

@api.depends("product_id", "product_id.business_impact_id")
def _compute_business_impact_id(self):
for record in self:
record.business_impact_id = record.product_id.business_impact_id

@api.depends("product_id.suggested_fee", "fee_factor")
def _compute_suggested_fee(self):
for record in self:
record.suggested_fee = record.product_id.suggested_fee * record.fee_factor

@api.depends(
"product_id",
"product_id.product_template_attribute_value_ids",
"product_id.product_template_attribute_value_ids.attribute_id",
)
def _compute_series_value_id(self):
for rec in self:
rec.series_value_id = False
if rec.product_id:
ptavs = rec.product_id.product_template_attribute_value_ids.filtered(
lambda v: v.attribute_id.is_odoo_series
)
if ptavs:
rec.series_value_id = ptavs[0].product_attribute_value_id

@api.constrains(
"partner_id",
"product_id",
"date_start",
"date_end",
"active",
)
def _check_date_overlap(self):
for record in self.filtered("active"):
if (
record.date_start
and record.date_end
and record.date_end < record.date_start
):
raise ValidationError(
_("End date cannot be earlier than the start date.")
)
if not record.partner_id or not record.product_id or not record.date_start:
continue
domain = [
("id", "!=", record.id),
("active", "=", True),
("partner_id", "=", record.partner_id.id),
("product_id", "=", record.product_id.id),
"|",
("date_end", "=", False),
("date_end", ">=", record.date_start),
]
if record.date_end:
domain += [
"|",
("date_start", "=", False),
("date_start", "<=", record.date_end),
]
overlap = self.search_count(domain)
if overlap:
raise ValidationError(
_(
"An active customer module already exists for this customer "
"and module in the selected period."
)
)
Loading
Loading