Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] sustainability_purchase: product's supplier emission factor preceding order #223

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions sustainability/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"views/carbon_factor_contributor.xml",
"views/carbon_factor_type.xml",
"views/product_category.xml",
"views/product_supplierinfo.xml",
"views/res_country.xml",
"views/res_config_settings.xml",
"views/sustainability_scenario.xml",
Expand Down
2 changes: 2 additions & 0 deletions sustainability/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from . import res_company
from . import res_config_settings
from . import res_partner

from . import account_account
from . import account_analytic_line
Expand All @@ -18,6 +19,7 @@
from . import product_category
from . import product_product
from . import product_template
from . import product_supplierinfo
from . import res_country
from . import carbon_factor_database
from . import carbon_factor_contributor
Expand Down
49 changes: 48 additions & 1 deletion sustainability/models/account_move_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ class AccountMoveLine(models.Model):
compute="_compute_is_carbon_positive", store=False, readonly=True
)

carbon_supplier_id = fields.Many2one(
comodel_name="product.supplierinfo",
string="Supplier Info",
compute="_compute_carbon_supplier_id",
)

def _compute_carbon_supplier_id(self):
"""
Compute the carbon_supplier_id field.
Note that since the same seller can be added multiple times to the same product,
we need to filter the sellers by the partner_id of the order.
If there are multiple matches,
we'll take the most recent one.
"""

for line in self:
seller = line.product_id.seller_ids.filtered(
lambda s: s.partner_id.id == line.move_id.partner_id.id # noqa: B023
)
line.carbon_supplier_id = seller[-1] if len(seller) > 1 else seller

def _prepare_analytic_distribution_line(
self, distribution, account_id, distribution_on_each_plan
) -> dict:
Expand Down Expand Up @@ -98,6 +119,14 @@ def _compute_carbon_is_date_locked(self):
# --------------------------------------------

@api.depends(
# Seller
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not optimized because it triggers recompute on all account.move.line when you change your seller_ids or the product. Same for company lock date.

"product_id.seller_ids",
"product_id.seller_ids.carbon_in_factor_id",
"move_id.partner_id",
# Partner
"partner_id",
"partner_id.carbon_in_factor_id",
# Other
"account_id.carbon_in_factor_id",
"product_id.carbon_in_factor_id",
"product_id.carbon_out_factor_id",
Expand Down Expand Up @@ -130,7 +159,25 @@ def _get_state_field_name(self) -> str:

@api.model
def _get_carbon_compute_possible_fields(self) -> list[str]:
return ["product_id", "account_id"]
return ["carbon_supplier_id", "product_id", "partner_id", "account_id"]

# --- Partner ---
def can_use_partner_id_carbon_value(self) -> bool:
self.ensure_one()
return self.move_id.is_outbound(include_receipts=True) and (
self.partner_id and self.partner_id.can_compute_carbon_value("in")
)

# --- Supplier ---
def can_use_carbon_supplier_id_carbon_value(self) -> bool:
self.ensure_one()
return bool(
self.carbon_supplier_id
) and self.carbon_supplier_id.can_compute_carbon_value("in")

def get_carbon_supplier_id_carbon_compute_values(self) -> dict:
self.ensure_one()
return self.get_product_id_carbon_compute_values()

def _get_carbon_compute_kwargs(self) -> dict:
res = super()._get_carbon_compute_kwargs()
Expand Down
28 changes: 27 additions & 1 deletion sustainability/models/carbon_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ class CarbonFactor(models.Model):
product_qty = fields.Integer(compute="_compute_product_qty")
product_categ_qty = fields.Integer(compute="_compute_product_categ_qty")
account_move_qty = fields.Integer(compute="_compute_account_move_qty")
contact_qty = fields.Integer(compute="_compute_contact_qty")
supplierinfo_qty = fields.Integer(compute="_compute_supplierinfo_qty")

# --------------------------------------------

Expand Down Expand Up @@ -185,6 +187,16 @@ def _compute_product_categ_qty(self):
for factor in self:
factor.product_categ_qty = count_data.get(factor.id, 0)

def _compute_contact_qty(self):
count_data = self._get_count_by_model(model="res.partner")
for factor in self:
factor.contact_qty = count_data.get(factor.id, 0)

def _compute_supplierinfo_qty(self):
count_data = self._get_count_by_model(model="product.supplierinfo")
for factor in self:
factor.supplierinfo_qty = count_data.get(factor.id, 0)

def _compute_carbon_currency_id(self):
for factor in self:
factor.carbon_currency_id = (
Expand Down Expand Up @@ -567,11 +579,25 @@ def action_see_product_categ_ids(self):
return self._generate_action(
title=_("Product Category for"),
model="product.category",
ids=self._get_distribution_lines_res_ids("product.template"),
ids=self._get_distribution_lines_res_ids("product.category"),
)

def action_see_account_move_ids(self):
origins = self.env["carbon.line.origin"].search([("factor_id", "in", self.ids)])
return self._generate_action(
title=_("Journal Entries"), model="account.move", ids=origins.move_id.ids
)

def action_see_contact_ids(self):
return self._generate_action(
title="Contact",
model="res.partner",
ids=self._get_distribution_lines_res_ids("res.partner"),
)

def action_see_supplierinfo_ids(self):
return self._generate_action(
title="Supplier Info",
model="product.supplierinfo",
ids=self._get_distribution_lines_res_ids("product.supplierinfo"),
)
2 changes: 2 additions & 0 deletions sustainability/models/carbon_line_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,14 @@ def get_carbon_sign(self) -> int:
"""
return 1

@api.model
def _get_computation_levels_mapping(self) -> dict:
return {
"account.move.line": _("Carbon on invoice"),
"product.product": _("Product"),
"product.category": _("Product category"),
"product.template": _("Product template"),
"product.supplierinfo": _("Supplier info"),
"res.partner": _("Partner"),
"account.account": _("Account"),
"res.company": _("Company fallback"),
Expand Down
30 changes: 20 additions & 10 deletions sustainability/models/carbon_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,6 @@


# Todo: make this extendable from sub modules
CARBON_MODELS = [
"carbon.factor",
"product.category",
"product.product",
"product.template",
"res.partner",
"res.company",
"res.country",
]


class CarbonMixin(models.AbstractModel):
Expand All @@ -55,6 +46,20 @@ class CarbonMixin(models.AbstractModel):
_carbon_types = ["in", "out"]
_fallback_records = []

# TODO: Thinks about compute this from env['carbon.line.mixin']._get_computation_levels_mapping()
@api.model
def _CARBON_MODELS(cls):
return [
"carbon.factor",
"product.category",
"product.product",
"product.supplierinfo",
"product.template",
"res.partner",
"res.company",
"res.country",
]

@api.constrains("carbon_in_use_distribution", "carbon_in_distribution_line_ids")
def _check_carbon_in_distribution(self):
for record in self.filtered("carbon_in_use_distribution"):
Expand All @@ -75,7 +80,9 @@ def _get_available_carbon_compute_methods(self) -> list[tuple[str, str]]:
@api.model
def _selection_fallback_model(self):
return [
(x, _(self.env[x]._description)) for x in CARBON_MODELS if x in self.env
(x, _(self.env[x]._description))
for x in self._CARBON_MODELS()
if x in self.env
]

def get_allowed_factors(self):
Expand Down Expand Up @@ -270,6 +277,9 @@ def _search_fallback_record(self, carbon_type: str) -> list[Any] | None:
self.ensure_one()
fallback_path = []
for rec in self._build_fallback_records_list(carbon_type):
# skip unsaved records with temporary IDs
if not rec.id or isinstance(rec.id, str) and rec.id.startswith("NewId"):
continue
fallback_path.append(rec)
if rec.has_valid_carbon_value(carbon_type):
return fallback_path
Expand Down
28 changes: 28 additions & 0 deletions sustainability/models/product_supplierinfo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from odoo import api, models


class ProductSupplierInfo(models.Model):
_name = "product.supplierinfo"
_inherit = ["product.supplierinfo", "carbon.mixin"]

def _update_carbon_in_fields(self, vals):
"""
Helper method to update carbon_in_is_manual and carbon_in_mode based on carbon_in_factor_id.
"""
if vals.get("carbon_in_factor_id"):
vals["carbon_in_is_manual"] = True
vals["carbon_in_mode"] = "manual"
else:
vals["carbon_in_is_manual"] = False
vals["carbon_in_mode"] = "auto"
return vals

@api.model
def create(self, vals):
vals = self._update_carbon_in_fields(vals)
return super().create(vals)

def write(self, vals):
if "carbon_in_factor_id" in vals:
vals = self._update_carbon_in_fields(vals)
return super().write(vals)
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,14 @@ def _cron_initial_carbon_compute_res_partner(self):
"sustainability_purchase.cron_initial_carbon_compute_res_partner"
)
_logger.warning(
"Please deactivate cron '%s' as it is not needed anymore."
% cron_id.name
f"Please deactivate cron '{cron_id.name}' as it is not needed anymore."
)
return

clock = time.perf_counter()
total = 0
_logger.info(
"Running _cron_initial_carbon_compute_res_partner on %s records"
% len(partners)
f"Running _cron_initial_carbon_compute_res_partner on {len(partners)} records"
)

for partner in partners:
Expand All @@ -73,8 +71,7 @@ def _cron_initial_carbon_compute_res_partner(self):
# Catch here any exceptions if you need to.
except Exception as e:
_logger.error(
"Error on cron _cron_initial_carbon_compute_res_partner : Exception: %s"
% e
f"Error on cron _cron_initial_carbon_compute_res_partner : Exception: {e}"
)

_logger.info(
Expand Down
2 changes: 2 additions & 0 deletions sustainability/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from . import test_conversion
from . import test_vendors_bill
from . import test_action
98 changes: 98 additions & 0 deletions sustainability/tests/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from datetime import datetime

from odoo import Command
from odoo.tests import TransactionCase


Expand Down Expand Up @@ -116,3 +117,100 @@ def setUpClass(cls):
"groups_id": [(6, 0, [cls.env.ref("base.group_user").id])],
}
)
cls.setUpClassVendor()

@classmethod
def setUpClassVendor(cls):
# Vendor Bills Related
# Vendor Carbon Factors
cls.vendor_carbon_factor_vendor_1 = cls.env["carbon.factor"].create(
dict(
name="Vendor Carbon Factor 1 (VENDOR)",
carbon_compute_method="monetary",
value_ids=[
Command.create(
dict(
date=datetime.today().strftime("%Y-%m-%d %H:%M"),
carbon_monetary_currency_id=cls.currency_usd.id,
carbon_value=5,
)
)
],
)
)
cls.vendor_carbon_factor_product_1 = cls.env["carbon.factor"].create(
dict(
name="Vendor Carbon Factor 1 (PRODUCT)",
carbon_compute_method="monetary",
value_ids=[
Command.create(
dict(
date=datetime.today().strftime("%Y-%m-%d %H:%M"),
carbon_monetary_currency_id=cls.currency_usd.id,
carbon_value=10,
)
)
],
)
)
# Vendor Partner
cls.vendor_partner_1 = cls.env["res.partner"].create(
dict(
name="Vendor Partner 1",
email="[email protected]",
phone="+123456789",
street="123 Vendor Street",
city="Vendor City",
country_id=cls.env.ref("base.us").id,
carbon_in_factor_id=cls.vendor_carbon_factor_vendor_1.id,
)
)

# Vendor Product Category
cls.vendor_product_category_1 = cls.env["product.category"].create(
dict(
name="Vendor Product Category 1",
)
)
# Vendor Product Template
cls.vendor_product_template_1 = cls.env["product.template"].create(
dict(
name="Vendor Product 1",
categ_id=cls.vendor_product_category_1.id,
list_price=100.0,
carbon_in_factor_id=cls.vendor_carbon_factor_product_1.id,
seller_ids=[
Command.create(
dict(
currency_id=cls.currency_usd.id,
delay=0,
min_qty=1,
partner_id=cls.vendor_partner_1.id,
price=100,
)
)
],
)
)
# Vendor Product Product
cls.vendor_product_product_1 = cls.env["product.product"].search(
[("product_tmpl_id", "=", cls.vendor_product_template_1.id)], limit=1
)
# Vendor Account Move
cls.vendor_account_move = cls.env["account.move"]
cls.vendor_account_move_1 = cls.env["account.move"].create(
dict(
move_type="in_invoice",
partner_id=cls.vendor_partner_1.id,
invoice_date=datetime.today().strftime("%Y-%m-%d"),
invoice_line_ids=[
Command.create(
dict(
product_id=cls.vendor_product_product_1.id,
quantity=10.0,
)
)
],
)
)
cls.vendor_account_move |= cls.vendor_account_move_1
Loading
Loading