Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
7
sale_product_identification/models/__init__.py
Executable file
7
sale_product_identification/models/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Copyright 2025 Binhex <https://www.binhex.cloud>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import product_template_id_category
|
||||
from . import product_template
|
||||
from . import res_partner_id_number
|
||||
from . import sale_order
|
||||
42
sale_product_identification/models/product_template.py
Executable file
42
sale_product_identification/models/product_template.py
Executable file
@@ -0,0 +1,42 @@
|
||||
# Copyright 2025 Binhex <https://www.binhex.cloud>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
required_identification = fields.Boolean(default=False)
|
||||
product_tmpl_category_ids = fields.One2many(
|
||||
"product.template.id_category", "product_tmpl_id"
|
||||
)
|
||||
|
||||
@api.constrains("product_tmpl_category_ids")
|
||||
def _check_product_tmpl_category_ids(self):
|
||||
ProductTemplateIdCategory = self.env["product.template.id_category"]
|
||||
for product_templ in self:
|
||||
category_ids = ProductTemplateIdCategory.sudo()._read_group(
|
||||
[
|
||||
("product_tmpl_id", "=", product_templ.id),
|
||||
],
|
||||
["category_id"],
|
||||
["category_id:count"],
|
||||
order="category_id:count DESC",
|
||||
)
|
||||
if category_ids and category_ids[0][1] > 1:
|
||||
category_ids = list(filter(lambda x: x[1] > 1, category_ids))
|
||||
raise ValidationError(
|
||||
_(
|
||||
"There are repeated categories in the identifications "
|
||||
"configuration, the quantities are shown below.\n%(categories)s"
|
||||
)
|
||||
% {
|
||||
"categories": "\n".join(
|
||||
f"{category[0].name}:\u2009{category[1]}"
|
||||
for category in category_ids
|
||||
)
|
||||
}
|
||||
)
|
||||
20
sale_product_identification/models/product_template_id_category.py
Executable file
20
sale_product_identification/models/product_template_id_category.py
Executable file
@@ -0,0 +1,20 @@
|
||||
# Copyright 2025 Binhex <https://www.binhex.cloud>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductTemplateIdcategory(models.Model):
|
||||
_name = "product.template.id_category"
|
||||
_description = "Product Template Identification Category"
|
||||
|
||||
product_tmpl_id = fields.Many2one("product.template")
|
||||
category_id = fields.Many2one("res.partner.id_category")
|
||||
is_mandatory = fields.Boolean(
|
||||
default=True, help="Defines whether identification is mandatory."
|
||||
)
|
||||
message = fields.Text(
|
||||
help="Allows you to define a description of why "
|
||||
"this identification is being added.\n"
|
||||
"Example: Asking the customer for identification"
|
||||
)
|
||||
109
sale_product_identification/models/res_partner_id_number.py
Executable file
109
sale_product_identification/models/res_partner_id_number.py
Executable file
@@ -0,0 +1,109 @@
|
||||
# Copyright 2025 Binhex <https://www.binhex.cloud>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ResPartnerIdNumber(models.Model):
|
||||
_inherit = "res.partner.id_number"
|
||||
|
||||
@api.model
|
||||
def message_error_identifications(
|
||||
self, product_tmpl_ids, diff_identification_ids, required=False
|
||||
):
|
||||
"""
|
||||
Define a message identifications by product
|
||||
|
||||
:param product_tmpl_ids: Products that have at least one identification
|
||||
category defined.
|
||||
:param diff_identification_ids: Required or optional identifications.
|
||||
:param required: Defines whether what is being validated is required or not.
|
||||
:return: A message with the identification categories per product.
|
||||
"""
|
||||
message = ""
|
||||
for product in product_tmpl_ids:
|
||||
identifications = product.mapped("product_tmpl_category_ids").filtered(
|
||||
lambda x: x.is_mandatory == required
|
||||
and x.category_id.id in diff_identification_ids
|
||||
)
|
||||
if identifications:
|
||||
message += _("\n%(product)s\n%(categories)s") % {
|
||||
"product": product.name,
|
||||
"categories": "\n".join(
|
||||
identifications.mapped(
|
||||
lambda x: f"\u2003\u2022\u2009"
|
||||
f"{x.category_id.name}\u2009"
|
||||
f"{f'({x.message})' if x.message else ''}"
|
||||
)
|
||||
),
|
||||
}
|
||||
return message
|
||||
|
||||
def _identification_domain(self, **params):
|
||||
"""
|
||||
Build a domain to filter valid identifications
|
||||
:param params: Dictionary of expected parameters
|
||||
:return: list: List of tuples representing the search domain
|
||||
"""
|
||||
user_tz = self.env.user.tz or self.env.context.get("tz")
|
||||
user_pytz = pytz.timezone(user_tz) if user_tz else pytz.utc
|
||||
now_dt = datetime.now().astimezone(user_pytz).date()
|
||||
if not params.get("partner_id", False):
|
||||
return []
|
||||
return [
|
||||
("partner_id", "=", params.get("partner_id")),
|
||||
"|",
|
||||
"|",
|
||||
"|",
|
||||
"&",
|
||||
("valid_from", "=", False),
|
||||
("valid_until", "=", False),
|
||||
"&",
|
||||
"&",
|
||||
("valid_from", "!=", False),
|
||||
("valid_until", "=", False),
|
||||
("valid_from", "<=", now_dt),
|
||||
"&",
|
||||
"&",
|
||||
"&",
|
||||
("valid_from", "!=", False),
|
||||
("valid_until", "!=", False),
|
||||
("valid_from", "<=", now_dt),
|
||||
("valid_until", ">=", now_dt),
|
||||
"&",
|
||||
"&",
|
||||
("valid_from", "=", False),
|
||||
("valid_until", "!=", False),
|
||||
("valid_until", ">=", now_dt),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def validate_identification(self, **params):
|
||||
"""
|
||||
Allows you to obtain the difference between 2 recordset IDs
|
||||
|
||||
:param params: A dictionary of values where at least
|
||||
the value 'compare_identification_ids' and 'partner_id'
|
||||
must be present to validate the identifications
|
||||
:return: A set of records with the difference between submitted
|
||||
identifications and those of the partner
|
||||
"""
|
||||
identification_ids = params.get("compare_identification_ids", set())
|
||||
partner_id = params.get("partner_id", False)
|
||||
if not partner_id:
|
||||
raise ValidationError(_("A client is required to verify identifications."))
|
||||
domain = self._identification_domain(**{"partner_id": partner_id})
|
||||
partner_identification_ids = self.env["res.partner.id_category"]
|
||||
for id_number in self.env["res.partner.id_number"].search(domain):
|
||||
id_number.category_id.validate_id_number(id_number)
|
||||
partner_identification_ids |= id_number.category_id
|
||||
return (
|
||||
identification_ids - partner_identification_ids
|
||||
if identification_ids
|
||||
else partner_identification_ids
|
||||
)
|
||||
115
sale_product_identification/models/sale_order.py
Executable file
115
sale_product_identification/models/sale_order.py
Executable file
@@ -0,0 +1,115 @@
|
||||
# Copyright 2025 Binhex <https://www.binhex.cloud>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import Command, _, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
def _diff_identification(self, compare_identification_ids):
|
||||
diff_identification = self.env["res.partner.id_number"].validate_identification(
|
||||
**{
|
||||
"compare_identification_ids": compare_identification_ids,
|
||||
"partner_id": self.partner_id.id,
|
||||
}
|
||||
)
|
||||
return diff_identification
|
||||
|
||||
def _message_error_identifications(self, diff_identification=False, required=False):
|
||||
"""
|
||||
Display messages created using the message_error_identifications
|
||||
method in order of sale.
|
||||
|
||||
:param diff_identification: Required or optional identifications.
|
||||
:return: Message by order
|
||||
"""
|
||||
message_error = ""
|
||||
for order in self:
|
||||
message_error = _("%(order)s%(message)s") % {
|
||||
"order": f"\n{order.name}" if len(self) > 1 else "",
|
||||
"message": self.env[
|
||||
"res.partner.id_number"
|
||||
].message_error_identifications(
|
||||
order.mapped("order_line.product_template_id").filtered(
|
||||
lambda product: product.required_identification
|
||||
and product.product_tmpl_category_ids
|
||||
),
|
||||
diff_identification,
|
||||
required,
|
||||
),
|
||||
}
|
||||
|
||||
return message_error
|
||||
|
||||
def _action_generate_confirm_identification(self, message):
|
||||
view = self.env.ref("sale_product_identification.confirm_identification_view")
|
||||
return {
|
||||
"name": _("Confirm identification"),
|
||||
"type": "ir.actions.act_window",
|
||||
"view_mode": "form",
|
||||
"res_model": "confirm.identification",
|
||||
"views": [(view.id, "form")],
|
||||
"view_id": view.id,
|
||||
"target": "new",
|
||||
"context": {
|
||||
"default_order_ids": [Command.set(self.ids)],
|
||||
"default_message": message,
|
||||
},
|
||||
}
|
||||
|
||||
def _get_domain_identifications(self, is_mandatory=False):
|
||||
return [
|
||||
("product_tmpl_id", "in", self.order_line.product_template_id.ids),
|
||||
("product_tmpl_id.required_identification", "=", True),
|
||||
("is_mandatory", "=", is_mandatory),
|
||||
]
|
||||
|
||||
def _validate_opt_identification(self):
|
||||
self.ensure_one()
|
||||
products_opt_identification_ids = (
|
||||
self.env["product.template.id_category"]
|
||||
.search(self._get_domain_identifications())
|
||||
.mapped("category_id")
|
||||
)
|
||||
if products_opt_identification_ids:
|
||||
message = _(
|
||||
"The following identifications require verification, "
|
||||
"please validate before continuing:\n %(identifications)s"
|
||||
) % {
|
||||
"identifications": self._message_error_identifications(
|
||||
products_opt_identification_ids.ids
|
||||
)
|
||||
}
|
||||
return self._action_generate_confirm_identification(message)
|
||||
return True
|
||||
|
||||
def _validate_identification(self):
|
||||
self.ensure_one()
|
||||
products_identification_ids = (
|
||||
self.env["product.template.id_category"]
|
||||
.search(self._get_domain_identifications(True))
|
||||
.mapped("category_id")
|
||||
)
|
||||
if products_identification_ids:
|
||||
diff_identification = self._diff_identification(products_identification_ids)
|
||||
if diff_identification:
|
||||
message = _(
|
||||
"The following identifications are required for "
|
||||
"partner, please verify.\n %(identifications)s"
|
||||
) % {
|
||||
"identifications": self._message_error_identifications(
|
||||
diff_identification.ids, True
|
||||
)
|
||||
}
|
||||
raise ValidationError(message)
|
||||
|
||||
def action_confirm(self):
|
||||
for order in self:
|
||||
order._validate_identification()
|
||||
if not self.env.context.get("not_verify_optional_identification", False):
|
||||
res = self[:1]._validate_opt_identification()
|
||||
if res is not True:
|
||||
return res
|
||||
return super().action_confirm()
|
||||
Reference in New Issue
Block a user