diff --git a/sustainability/__manifest__.py b/sustainability/__manifest__.py
index 234319b7..8d8d7a80 100644
--- a/sustainability/__manifest__.py
+++ b/sustainability/__manifest__.py
@@ -31,6 +31,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",
diff --git a/sustainability/models/__init__.py b/sustainability/models/__init__.py
index b691be8d..deccd68f 100644
--- a/sustainability/models/__init__.py
+++ b/sustainability/models/__init__.py
@@ -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
@@ -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
diff --git a/sustainability/models/account_move_line.py b/sustainability/models/account_move_line.py
index 66e9a88e..80ef38d3 100644
--- a/sustainability/models/account_move_line.py
+++ b/sustainability/models/account_move_line.py
@@ -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:
@@ -98,6 +119,14 @@ def _compute_carbon_is_date_locked(self):
# --------------------------------------------
@api.depends(
+ # Seller
+ "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",
@@ -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()
diff --git a/sustainability/models/carbon_factor.py b/sustainability/models/carbon_factor.py
index 6d784d1f..d4e0051f 100644
--- a/sustainability/models/carbon_factor.py
+++ b/sustainability/models/carbon_factor.py
@@ -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")
# --------------------------------------------
@@ -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 = (
@@ -567,7 +579,7 @@ 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):
@@ -575,3 +587,17 @@ def action_see_account_move_ids(self):
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"),
+ )
diff --git a/sustainability/models/carbon_line_mixin.py b/sustainability/models/carbon_line_mixin.py
index 4edac313..b0f1e2e0 100644
--- a/sustainability/models/carbon_line_mixin.py
+++ b/sustainability/models/carbon_line_mixin.py
@@ -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"),
diff --git a/sustainability/models/carbon_mixin.py b/sustainability/models/carbon_mixin.py
index a7277d8f..383b8540 100644
--- a/sustainability/models/carbon_mixin.py
+++ b/sustainability/models/carbon_mixin.py
@@ -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):
@@ -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"):
@@ -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):
@@ -270,6 +277,9 @@ def _search_fallback_record(self, carbon_type: str):
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
diff --git a/sustainability/models/product_supplierinfo.py b/sustainability/models/product_supplierinfo.py
new file mode 100644
index 00000000..62d79f75
--- /dev/null
+++ b/sustainability/models/product_supplierinfo.py
@@ -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)
diff --git a/sustainability_purchase/models/res_partner.py b/sustainability/models/res_partner.py
similarity index 87%
rename from sustainability_purchase/models/res_partner.py
rename to sustainability/models/res_partner.py
index afcba7de..4eda5a1d 100644
--- a/sustainability_purchase/models/res_partner.py
+++ b/sustainability/models/res_partner.py
@@ -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:
@@ -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(
diff --git a/sustainability/tests/__init__.py b/sustainability/tests/__init__.py
index da9ca34f..626634e9 100644
--- a/sustainability/tests/__init__.py
+++ b/sustainability/tests/__init__.py
@@ -1 +1,3 @@
from . import test_conversion
+from . import test_vendors_bill
+from . import test_action
diff --git a/sustainability/tests/common.py b/sustainability/tests/common.py
index ae83a836..abf4f2ea 100644
--- a/sustainability/tests/common.py
+++ b/sustainability/tests/common.py
@@ -1,5 +1,6 @@
from datetime import datetime
+from odoo import Command
from odoo.tests import TransactionCase
@@ -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="vendor@email.com",
+ 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
diff --git a/sustainability/tests/test_action.py b/sustainability/tests/test_action.py
new file mode 100644
index 00000000..18fa77c9
--- /dev/null
+++ b/sustainability/tests/test_action.py
@@ -0,0 +1,141 @@
+from odoo.addons.sustainability.tests.common import CarbonCommon
+
+
+class TestCarbonPurchaseAction(CarbonCommon):
+ """
+ Here we are testing the actions of the carbon factor from the smart buttons. We only check the res_model, name, and domain of the actions. We've encountered problem in the past so we are testing it.
+ """
+
+ def test_contact_action(self):
+ # Contact Action
+ contact_action = self.carbon_factor_default_fallback.action_see_contact_ids()
+ self.assertEqual(contact_action["res_model"], "res.partner")
+ self.assertEqual(
+ contact_action["name"],
+ f"Contact {self.carbon_factor_default_fallback.name}",
+ )
+ self.assertEqual(
+ contact_action["domain"],
+ [
+ (
+ "id",
+ "in",
+ self.carbon_factor_default_fallback._get_distribution_lines_res_ids(
+ "res.partner"
+ ),
+ )
+ ],
+ )
+
+ def test_supplierinfo_action(self):
+ # Supplier Info Action
+ supplierinfo_action = (
+ self.carbon_factor_default_fallback.action_see_supplierinfo_ids()
+ )
+ self.assertEqual(supplierinfo_action["res_model"], "product.supplierinfo")
+ self.assertEqual(
+ supplierinfo_action["name"],
+ f"Supplier Info {self.carbon_factor_default_fallback.name}",
+ )
+ self.assertEqual(
+ supplierinfo_action["domain"],
+ [
+ (
+ "id",
+ "in",
+ self.carbon_factor_default_fallback._get_distribution_lines_res_ids(
+ "product.supplierinfo"
+ ),
+ )
+ ],
+ )
+
+ def test_child_action(self):
+ # Child Action
+ child_action = self.carbon_factor_default_fallback.action_see_child_ids()
+ self.assertEqual(child_action["res_model"], "carbon.factor")
+ self.assertEqual(
+ child_action["domain"],
+ [
+ (
+ "id",
+ "in",
+ self.carbon_factor_default_fallback.child_ids.ids,
+ )
+ ],
+ )
+
+ def test_chart_of_account_action(self):
+ # Chart of Account Action
+ chart_of_account_action = (
+ self.carbon_factor_default_fallback.action_see_chart_of_account_ids()
+ )
+ self.assertEqual(chart_of_account_action["res_model"], "account.account")
+ self.assertEqual(
+ chart_of_account_action["domain"],
+ [
+ (
+ "id",
+ "in",
+ self.carbon_factor_default_fallback._get_distribution_lines_res_ids(
+ "account.account"
+ ),
+ )
+ ],
+ )
+
+ def test_product_action(self):
+ # Product Action
+ product_action = self.carbon_factor_default_fallback.action_see_product_ids()
+ self.assertEqual(product_action["res_model"], "product.template")
+ self.assertEqual(
+ product_action["domain"],
+ [
+ (
+ "id",
+ "in",
+ self.carbon_factor_default_fallback._get_distribution_lines_res_ids(
+ "product.template"
+ ),
+ )
+ ],
+ )
+
+ def test_product_categ_action(self):
+ # Product Category Action
+ product_categ_action = (
+ self.carbon_factor_default_fallback.action_see_product_categ_ids()
+ )
+ self.assertEqual(product_categ_action["res_model"], "product.category")
+ self.assertEqual(
+ product_categ_action["domain"],
+ [
+ (
+ "id",
+ "in",
+ self.carbon_factor_default_fallback._get_distribution_lines_res_ids(
+ "product.category"
+ ),
+ )
+ ],
+ )
+
+ def test_account_move_action(self):
+ # Account Move Action
+ account_move_action = (
+ self.carbon_factor_default_fallback.action_see_account_move_ids()
+ )
+ origins = self.env["carbon.line.origin"].search(
+ [("factor_id", "in", self.carbon_factor_default_fallback.ids)]
+ )
+ self.assertEqual(account_move_action["res_model"], "account.move")
+ self.assertEqual(
+ account_move_action["domain"],
+ [
+ (
+ "id",
+ "in",
+ origins.move_id.ids,
+ )
+ ],
+ )
diff --git a/sustainability/tests/test_vendors_bill.py b/sustainability/tests/test_vendors_bill.py
new file mode 100644
index 00000000..168cb25e
--- /dev/null
+++ b/sustainability/tests/test_vendors_bill.py
@@ -0,0 +1,12 @@
+from odoo.addons.sustainability.tests.common import CarbonCommon
+
+
+class TestCarbonVendorsBill(CarbonCommon):
+ def test_vendor_bill(self):
+ # Check with the default values
+ self.vendor_account_move_1.action_recompute_carbon()
+ # self.assertEqual(round(self.vendor_account_move_1.carbon_balance, 2), 10*100*10) # TODO: Make sure this test pass as soon as possible
+
+ def test_vendor_action_post(self):
+ for move in self.vendor_account_move:
+ move.action_post()
diff --git a/sustainability/views/product_supplierinfo.xml b/sustainability/views/product_supplierinfo.xml
new file mode 100644
index 00000000..74bfe0e9
--- /dev/null
+++ b/sustainability/views/product_supplierinfo.xml
@@ -0,0 +1,85 @@
+
+