diff --git a/product_odoo_module/README.md b/product_odoo_module/README.md new file mode 100644 index 0000000..7779ace --- /dev/null +++ b/product_odoo_module/README.md @@ -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. diff --git a/product_odoo_module/__init__.py b/product_odoo_module/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/product_odoo_module/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_odoo_module/__manifest__.py b/product_odoo_module/__manifest__.py new file mode 100644 index 0000000..c15e27b --- /dev/null +++ b/product_odoo_module/__manifest__.py @@ -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, +} diff --git a/product_odoo_module/data/module_business_impact_data.xml b/product_odoo_module/data/module_business_impact_data.xml new file mode 100644 index 0000000..3e39602 --- /dev/null +++ b/product_odoo_module/data/module_business_impact_data.xml @@ -0,0 +1,18 @@ + + + + Tier-A + Mission Critical + 10 + + + Tier-B + Major + 20 + + + Tier-C + Minor + 30 + + diff --git a/product_odoo_module/data/module_complexity_data.xml b/product_odoo_module/data/module_complexity_data.xml new file mode 100644 index 0000000..d3e22f2 --- /dev/null +++ b/product_odoo_module/data/module_complexity_data.xml @@ -0,0 +1,18 @@ + + + + High + 1 + 1.2 + + + Medium + 2 + 1.0 + + + Low + 3 + 0.8 + + diff --git a/product_odoo_module/data/module_license_data.xml b/product_odoo_module/data/module_license_data.xml new file mode 100644 index 0000000..207fa58 --- /dev/null +++ b/product_odoo_module/data/module_license_data.xml @@ -0,0 +1,23 @@ + + + + AGPL-3 + 10 + 0.8 + + + LGPL-3 + 20 + 0.8 + + + GPL-3 + 30 + 0.8 + + + Other Proprietary + 40 + 1.0 + + diff --git a/product_odoo_module/data/module_popularity_data.xml b/product_odoo_module/data/module_popularity_data.xml new file mode 100644 index 0000000..379efd0 --- /dev/null +++ b/product_odoo_module/data/module_popularity_data.xml @@ -0,0 +1,28 @@ + + + + Very High + 1 + 0.2 + + + High + 2 + 0.4 + + + Medium + 3 + 0.6 + + + Low + 4 + 0.8 + + + Very Low + 5 + 1.0 + + diff --git a/product_odoo_module/data/module_source_type_data.xml b/product_odoo_module/data/module_source_type_data.xml new file mode 100644 index 0000000..9752c29 --- /dev/null +++ b/product_odoo_module/data/module_source_type_data.xml @@ -0,0 +1,23 @@ + + + + OCA + 10 + 0.3 + + + Our Team + 20 + 0.5 + + + User In-house + 30 + 0.5 + + + Third Party + 40 + 1.0 + + diff --git a/product_odoo_module/data/product_attribute_data.xml b/product_odoo_module/data/product_attribute_data.xml new file mode 100644 index 0000000..7bf8735 --- /dev/null +++ b/product_odoo_module/data/product_attribute_data.xml @@ -0,0 +1,48 @@ + + + + Series + always + + + + 10.0 + + + + 11.0 + + + + 12.0 + + + + 13.0 + + + + 14.0 + + + + 15.0 + + + + 16.0 + + + + 17.0 + + + + 18.0 + + + + 19.0 + + + diff --git a/product_odoo_module/models/__init__.py b/product_odoo_module/models/__init__.py new file mode 100644 index 0000000..075b662 --- /dev/null +++ b/product_odoo_module/models/__init__.py @@ -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 diff --git a/product_odoo_module/models/customer_module_info.py b/product_odoo_module/models/customer_module_info.py new file mode 100644 index 0000000..2220600 --- /dev/null +++ b/product_odoo_module/models/customer_module_info.py @@ -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." + ) + ) diff --git a/product_odoo_module/models/module_business_impact.py b/product_odoo_module/models/module_business_impact.py new file mode 100644 index 0000000..c01feb4 --- /dev/null +++ b/product_odoo_module/models/module_business_impact.py @@ -0,0 +1,15 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ModuleBusinessImpact(models.Model): + _name = "module.business.impact" + _description = "Module Business Impact" + _order = "sequence, name" + + name = fields.Char(required=True) + description = fields.Char() + sequence = fields.Integer(default=10) + active = fields.Boolean(default=True) diff --git a/product_odoo_module/models/module_complexity.py b/product_odoo_module/models/module_complexity.py new file mode 100644 index 0000000..315ae5e --- /dev/null +++ b/product_odoo_module/models/module_complexity.py @@ -0,0 +1,15 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ModuleComplexity(models.Model): + _name = "module.complexity" + _description = "Module Complexity" + _order = "sequence, id" + + name = fields.Char(required=True, translate=True) + sequence = fields.Integer(default=10) + factor = fields.Float(default=1.0, help="Multiplier for fee calculation.") + active = fields.Boolean(default=True) diff --git a/product_odoo_module/models/module_license.py b/product_odoo_module/models/module_license.py new file mode 100644 index 0000000..70307af --- /dev/null +++ b/product_odoo_module/models/module_license.py @@ -0,0 +1,15 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ModuleLicense(models.Model): + _name = "module.license" + _description = "Module License" + _order = "sequence, name" + + name = fields.Char(required=True) + sequence = fields.Integer(default=10) + factor = fields.Float(default=1.0, help="Multiplier for fee calculation.") + active = fields.Boolean(default=True) diff --git a/product_odoo_module/models/module_popularity.py b/product_odoo_module/models/module_popularity.py new file mode 100644 index 0000000..3ffe238 --- /dev/null +++ b/product_odoo_module/models/module_popularity.py @@ -0,0 +1,15 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ModulePopularity(models.Model): + _name = "module.popularity" + _description = "Module Popularity" + _order = "sequence, id" + + name = fields.Char(required=True, translate=True) + sequence = fields.Integer(default=10) + factor = fields.Float(default=1.0, help="Multiplier for fee calculation.") + active = fields.Boolean(default=True) diff --git a/product_odoo_module/models/module_source_type.py b/product_odoo_module/models/module_source_type.py new file mode 100644 index 0000000..2d132c0 --- /dev/null +++ b/product_odoo_module/models/module_source_type.py @@ -0,0 +1,15 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ModuleSourceType(models.Model): + _name = "module.source.type" + _description = "Module Source Type" + _order = "sequence, name" + + name = fields.Char(required=True, translate=True) + sequence = fields.Integer(default=10) + factor = fields.Float(default=1.0, help="Multiplier for fee calculation.") + active = fields.Boolean(default=True) diff --git a/product_odoo_module/models/product_attribute.py b/product_odoo_module/models/product_attribute.py new file mode 100644 index 0000000..9b9046d --- /dev/null +++ b/product_odoo_module/models/product_attribute.py @@ -0,0 +1,12 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductAttribute(models.Model): + _inherit = "product.attribute" + + is_odoo_series = fields.Boolean( + help="Indicates whether this attribute represents the Odoo version series.", + ) diff --git a/product_odoo_module/models/product_product.py b/product_odoo_module/models/product_product.py new file mode 100644 index 0000000..c4f18a7 --- /dev/null +++ b/product_odoo_module/models/product_product.py @@ -0,0 +1,81 @@ +# 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 + + +class ProductProduct(models.Model): + _inherit = "product.product" + + is_odoo_module = fields.Boolean(related="product_tmpl_id.is_odoo_module") + source_type_id = fields.Many2one("module.source.type", string="Source Type") + module_complexity_id = fields.Many2one("module.complexity", string="Complexity") + module_license_id = fields.Many2one("module.license", string="License") + loc = fields.Integer(string="Lines of Code") + suggested_fee = fields.Float( + compute="_compute_suggested_fee", + help="Suggested monthly fee computed from company formula.", + ) + + @api.model_create_multi + def create(self, vals_list): + products = super().create(vals_list) + for product in products: + vals = {} + if not product.source_type_id and product.product_tmpl_id.source_type_id: + vals["source_type_id"] = product.product_tmpl_id.source_type_id.id + if ( + not product.module_complexity_id + and product.product_tmpl_id.module_complexity_id + ): + vals[ + "module_complexity_id" + ] = product.product_tmpl_id.module_complexity_id.id + if ( + not product.module_license_id + and product.product_tmpl_id.module_license_id + ): + vals["module_license_id"] = product.product_tmpl_id.module_license_id.id + if vals: + product.write(vals) + return products + + @api.depends( + "loc", + "source_type_id", + "module_license_id", + "module_complexity_id", + "module_popularity_id", + "customer_module_count", + ) + def _compute_suggested_fee(self): + company = self.env.company + standard_rate = company.module_standard_rate or 0.0 + for rec in self: + source_type_factor = rec.source_type_id.factor or 1.0 + license_factor = rec.module_license_id.factor or 1.0 + complexity_factor = rec.module_complexity_id.factor or 1.0 + popularity_factor = rec.module_popularity_id.factor or 1.0 + customer_count = rec.customer_module_count or 1 + rec.suggested_fee = ( + 100 + + rec.loc + / 100 + * standard_rate + * source_type_factor + * complexity_factor + * license_factor + * popularity_factor + / (customer_count * 0.7) + ) + + def action_view_customer_modules(self): + self.ensure_one() + action = self.env.ref("product_odoo_module.action_customer_module").read()[0] + action["domain"] = [("product_tmpl_id", "=", self.product_tmpl_id.id)] + action["context"] = dict( + self.env.context, + default_product_tmpl_id=self.product_tmpl_id.id, + search_default_active=1, + ) + return action diff --git a/product_odoo_module/models/product_template.py b/product_odoo_module/models/product_template.py new file mode 100644 index 0000000..85151ce --- /dev/null +++ b/product_odoo_module/models/product_template.py @@ -0,0 +1,53 @@ +# 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 + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_odoo_module = fields.Boolean( + default=False, + help="Enable this for products that represent maintained Odoo modules. " + "Module products are typically Service products (no stock).", + ) + source_type_id = fields.Many2one("module.source.type", string="Source Type") + module_technical_name = fields.Char(string="Technical Name") + module_repo_url = fields.Char(string="Repository URL") + module_notes = fields.Text(string="Notes") + business_impact_id = fields.Many2one("module.business.impact") + module_license_id = fields.Many2one("module.license", string="License") + module_complexity_id = fields.Many2one( + "module.complexity", + string="Complexity", + ) + module_popularity_id = fields.Many2one("module.popularity", string="Popularity") + customer_module_count = fields.Integer(compute="_compute_customer_module_count") + + @api.depends("product_variant_ids") + def _compute_customer_module_count(self): + counts = {} + if self.ids: + data = self.env["customer.module.info"].read_group( + [("product_tmpl_id", "in", self.ids), ("active", "=", True)], + ["product_tmpl_id"], + ["product_tmpl_id"], + ) + counts = { + item["product_tmpl_id"][0]: item["product_tmpl_id_count"] + for item in data + } + for record in self: + record.customer_module_count = counts.get(record.id, 0) + + def action_view_customer_modules(self): + self.ensure_one() + action = self.env.ref("product_odoo_module.action_customer_module").read()[0] + action["domain"] = [("product_tmpl_id", "=", self.id)] + action["context"] = dict( + self.env.context, + default_product_tmpl_id=self.id, + search_default_active=1, + ) + return action diff --git a/product_odoo_module/models/res_company.py b/product_odoo_module/models/res_company.py new file mode 100644 index 0000000..fc48226 --- /dev/null +++ b/product_odoo_module/models/res_company.py @@ -0,0 +1,19 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + module_fee_formula = fields.Text( + help="Python expression to compute suggested monthly fee. " + "Available variables: base_price, source_type_code, customer_count, " + "license_name, loc, standard_rate. " + "Example: base_price * (0.8 if source_type_code == 'oca' else 1.0)", + ) + module_standard_rate = fields.Monetary( + string="Standard Rate per 100 LOC", + help="Standard price per 100 lines of code for module fee calculation.", + ) diff --git a/product_odoo_module/models/res_partner.py b/product_odoo_module/models/res_partner.py new file mode 100644 index 0000000..67ffb92 --- /dev/null +++ b/product_odoo_module/models/res_partner.py @@ -0,0 +1,34 @@ +# 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 + + +class ResPartner(models.Model): + _inherit = "res.partner" + + customer_module_count = fields.Integer(compute="_compute_customer_module_count") + + @api.depends("child_ids") + def _compute_customer_module_count(self): + counts = {} + if self.ids: + data = self.env["customer.module.info"].read_group( + [("partner_id", "in", self.ids), ("active", "=", True)], + ["partner_id"], + ["partner_id"], + ) + counts = {item["partner_id"][0]: item["partner_id_count"] for item in data} + for partner in self: + partner.customer_module_count = counts.get(partner.id, 0) + + def action_view_customer_modules(self): + self.ensure_one() + action = self.env.ref("product_odoo_module.action_customer_module").read()[0] + action["domain"] = [("partner_id", "=", self.id)] + action["context"] = dict( + self.env.context, + default_partner_id=self.id, + search_default_active=1, + ) + return action diff --git a/product_odoo_module/security/ir.model.access.csv b/product_odoo_module/security/ir.model.access.csv new file mode 100644 index 0000000..c4eb7df --- /dev/null +++ b/product_odoo_module/security/ir.model.access.csv @@ -0,0 +1,19 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_customer_module_info_user,customer.module.info user,model_customer_module_info,base.group_user,1,0,0,0 +access_customer_module_info_portal,customer.module.info portal,model_customer_module_info,base.group_portal,1,0,0,0 +access_customer_module_info_manager,customer.module.info manager,model_customer_module_info,product_odoo_module.group_product_odoo_module_manager,1,1,1,1 +access_module_complexity_user,module.complexity user,model_module_complexity,base.group_user,1,0,0,0 +access_module_complexity_portal,module.complexity portal,model_module_complexity,base.group_portal,1,0,0,0 +access_module_complexity_manager,module.complexity manager,model_module_complexity,product_odoo_module.group_product_odoo_module_manager,1,1,1,1 +access_module_license_user,module.license user,model_module_license,base.group_user,1,0,0,0 +access_module_license_portal,module.license portal,model_module_license,base.group_portal,1,0,0,0 +access_module_license_manager,module.license manager,model_module_license,product_odoo_module.group_product_odoo_module_manager,1,1,1,1 +access_module_popularity_user,module.popularity user,model_module_popularity,base.group_user,1,0,0,0 +access_module_popularity_portal,module.popularity portal,model_module_popularity,base.group_portal,1,0,0,0 +access_module_popularity_manager,module.popularity manager,model_module_popularity,product_odoo_module.group_product_odoo_module_manager,1,1,1,1 +access_module_business_impact_user,module.business.impact user,model_module_business_impact,base.group_user,1,0,0,0 +access_module_business_impact_portal,module.business.impact portal,model_module_business_impact,base.group_portal,1,0,0,0 +access_module_business_impact_manager,module.business.impact manager,model_module_business_impact,product_odoo_module.group_product_odoo_module_manager,1,1,1,1 +access_module_source_type_user,module.source.type user,model_module_source_type,base.group_user,1,0,0,0 +access_module_source_type_portal,module.source.type portal,model_module_source_type,base.group_portal,1,0,0,0 +access_module_source_type_manager,module.source.type manager,model_module_source_type,product_odoo_module.group_product_odoo_module_manager,1,1,1,1 diff --git a/product_odoo_module/security/security.xml b/product_odoo_module/security/security.xml new file mode 100644 index 0000000..921cdfb --- /dev/null +++ b/product_odoo_module/security/security.xml @@ -0,0 +1,18 @@ + + + + Odoo Module Registry + 30 + + + Odoo Module Manager + + + + + Customer Module Info: multi-company + + + [('company_id', 'in', company_ids + [False])] + + diff --git a/product_odoo_module/tests/__init__.py b/product_odoo_module/tests/__init__.py new file mode 100644 index 0000000..0cbde8f --- /dev/null +++ b/product_odoo_module/tests/__init__.py @@ -0,0 +1 @@ +from . import test_customer_module diff --git a/product_odoo_module/tests/test_customer_module.py b/product_odoo_module/tests/test_customer_module.py new file mode 100644 index 0000000..9108f54 --- /dev/null +++ b/product_odoo_module/tests/test_customer_module.py @@ -0,0 +1,48 @@ +from datetime import date + +from odoo.exceptions import ValidationError +from odoo.tests import common + + +class TestCustomerModule(common.TransactionCase): + def setUp(self): + super().setUp() + self.partner = self.env["res.partner"].create({"name": "Test Partner"}) + self.product_template = self.env["product.template"].create( + { + "name": "Module A", + "detailed_type": "service", + } + ) + self.product_variant = self.product_template.product_variant_id + + def test_overlap_constraint(self): + model = self.env["customer.module.info"] + model.create( + { + "partner_id": self.partner.id, + "product_id": self.product_variant.id, + "monthly_fee": 100.0, + "date_start": date(2026, 1, 1), + "date_end": date(2026, 6, 30), + } + ) + with self.assertRaises(ValidationError): + model.create( + { + "partner_id": self.partner.id, + "product_id": self.product_variant.id, + "monthly_fee": 120.0, + "date_start": date(2026, 6, 1), + "date_end": date(2026, 12, 31), + } + ) + model.create( + { + "partner_id": self.partner.id, + "product_id": self.product_variant.id, + "monthly_fee": 120.0, + "date_start": date(2026, 7, 1), + "date_end": date(2026, 12, 31), + } + ) diff --git a/product_odoo_module/views/customer_module_info_views.xml b/product_odoo_module/views/customer_module_info_views.xml new file mode 100644 index 0000000..8b87357 --- /dev/null +++ b/product_odoo_module/views/customer_module_info_views.xml @@ -0,0 +1,129 @@ + + + + customer.module.info.tree + customer.module.info + + + + + + + + + + + + + + + + + + + + + + customer.module.info.form + customer.module.info + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + customer.module.info.search + customer.module.info + + + + + + + + + + + + + + + + + + + Customer Modules + customer.module.info + tree,form + + {'search_default_active': 1} + + + Odoo Module Templates + product.template + tree,form + [("is_odoo_module","=",True)] + {'search_default_is_odoo_module': 1} + + + Odoo Modules + product.product + tree,form + [("is_odoo_module","=",True)] + +
diff --git a/product_odoo_module/views/module_business_impact_views.xml b/product_odoo_module/views/module_business_impact_views.xml new file mode 100644 index 0000000..2f3beff --- /dev/null +++ b/product_odoo_module/views/module_business_impact_views.xml @@ -0,0 +1,56 @@ + + + + module.business.impact.view.tree + module.business.impact + + + + + + + + + + module.business.impact.view.form + module.business.impact + +
+ + + + + + + + + + +
+
+ + module.business.impact.view.search + module.business.impact + + + + + + + + + Business Impacts + module.business.impact + tree,form + + +
diff --git a/product_odoo_module/views/module_complexity_views.xml b/product_odoo_module/views/module_complexity_views.xml new file mode 100644 index 0000000..b497530 --- /dev/null +++ b/product_odoo_module/views/module_complexity_views.xml @@ -0,0 +1,41 @@ + + + + module.complexity.tree + module.complexity + + + + + + + + + + module.complexity.form + module.complexity + +
+ + + + + + + + + + +
+
+ + Module Complexity + module.complexity + tree,form + +
diff --git a/product_odoo_module/views/module_license_views.xml b/product_odoo_module/views/module_license_views.xml new file mode 100644 index 0000000..65b0c01 --- /dev/null +++ b/product_odoo_module/views/module_license_views.xml @@ -0,0 +1,56 @@ + + + + module.license.view.tree + module.license + + + + + + + + + + module.license.view.form + module.license + +
+ + + + + + + + + + +
+
+ + module.license.view.search + module.license + + + + + + + + + Licenses + module.license + tree,form + + +
diff --git a/product_odoo_module/views/module_popularity_views.xml b/product_odoo_module/views/module_popularity_views.xml new file mode 100644 index 0000000..90c7bb6 --- /dev/null +++ b/product_odoo_module/views/module_popularity_views.xml @@ -0,0 +1,41 @@ + + + + module.popularity.tree + module.popularity + + + + + + + + + + module.popularity.form + module.popularity + +
+ + + + + + + + + + +
+
+ + Module Popularity + module.popularity + tree,form + +
diff --git a/product_odoo_module/views/module_source_type_views.xml b/product_odoo_module/views/module_source_type_views.xml new file mode 100644 index 0000000..5fe51d5 --- /dev/null +++ b/product_odoo_module/views/module_source_type_views.xml @@ -0,0 +1,56 @@ + + + + module.source.type.view.tree + module.source.type + + + + + + + + + + module.source.type.view.form + module.source.type + +
+ + + + + + + + + + +
+
+ + module.source.type.view.search + module.source.type + + + + + + + + + Source Types + module.source.type + tree,form + + +
diff --git a/product_odoo_module/views/product_attribute_views.xml b/product_odoo_module/views/product_attribute_views.xml new file mode 100644 index 0000000..cf1bdb9 --- /dev/null +++ b/product_odoo_module/views/product_attribute_views.xml @@ -0,0 +1,15 @@ + + + + product.attribute.view.form.inherit.product_odoo_module + product.attribute + + + + + + + + diff --git a/product_odoo_module/views/product_odoo_module_menuitems.xml b/product_odoo_module/views/product_odoo_module_menuitems.xml new file mode 100644 index 0000000..301e236 --- /dev/null +++ b/product_odoo_module/views/product_odoo_module_menuitems.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + diff --git a/product_odoo_module/views/product_product_views.xml b/product_odoo_module/views/product_product_views.xml new file mode 100644 index 0000000..88fd79a --- /dev/null +++ b/product_odoo_module/views/product_product_views.xml @@ -0,0 +1,21 @@ + + + + product.product.view.form + product.product + + + + + + + + + + diff --git a/product_odoo_module/views/product_template_views.xml b/product_odoo_module/views/product_template_views.xml new file mode 100644 index 0000000..06c94c6 --- /dev/null +++ b/product_odoo_module/views/product_template_views.xml @@ -0,0 +1,46 @@ + + + + product.template.form.view + product.template + + + + + + + + + + + + + + + + + + + + + + diff --git a/product_odoo_module/views/res_company_views.xml b/product_odoo_module/views/res_company_views.xml new file mode 100644 index 0000000..4354bc5 --- /dev/null +++ b/product_odoo_module/views/res_company_views.xml @@ -0,0 +1,26 @@ + + + + res.company.form.odoo.module + res.company + + + + + + + + + + +
+ Available variables: source_type_factor, complexity_factor, license_factor, customer_count, loc, standard_rate +
+
+
+
+
+
diff --git a/product_odoo_module/views/res_partner_views.xml b/product_odoo_module/views/res_partner_views.xml new file mode 100644 index 0000000..aaeb7a6 --- /dev/null +++ b/product_odoo_module/views/res_partner_views.xml @@ -0,0 +1,24 @@ + + + + res.partner.view.form + res.partner + + + + + + + + diff --git a/setup/product_odoo_module/odoo/addons/product_odoo_module b/setup/product_odoo_module/odoo/addons/product_odoo_module new file mode 120000 index 0000000..13a790c --- /dev/null +++ b/setup/product_odoo_module/odoo/addons/product_odoo_module @@ -0,0 +1 @@ +../../../../product_odoo_module \ No newline at end of file diff --git a/setup/product_odoo_module/setup.py b/setup/product_odoo_module/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/product_odoo_module/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)