Skip to content

Commit

Permalink
[REF] project_consumable: improve consumable detection
Browse files Browse the repository at this point in the history
In the previous implementation project_id field was reused on account move line
but a lot of the odoo code source assume that an account move line with a project_id
is a timesheet, making very hard to distinguish Materials and Timesheet.

Adding a consumable_project_id it avoid to breaks existing code make module
much more easier to maintains
  • Loading branch information
petrus-v committed Feb 4, 2025
1 parent 293b4f9 commit 0007d75
Show file tree
Hide file tree
Showing 22 changed files with 297 additions and 375 deletions.
14 changes: 11 additions & 3 deletions project_consumable/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ Project consumable

|badge1| |badge2| |badge3| |badge4| |badge5|

This module provides a 'closed' flag on project task stages.
This module allow to collect materials/consumable linked to a project
adding account analytic lines.

**Table of contents**

Expand Down Expand Up @@ -65,7 +66,6 @@ quantities and Unit of Mesure provided by users, analytic amount will be
computed based on product cost.

- Material & Consumable Menu
- On task tab
- Project tab

Review consumable amount
Expand Down Expand Up @@ -98,7 +98,7 @@ Authors
Contributors
------------

- Pierre Verkest <[email protected]>
- Pierre Verkest <[email protected]>

Maintainers
-----------
Expand All @@ -113,6 +113,14 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-petrus-v| image:: https://github.com/petrus-v.png?size=40px
:target: https://github.com/petrus-v
:alt: petrus-v

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-petrus-v|

This module is part of the `OCA/project <https://github.com/OCA/project/tree/17.0/project_consumable>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 1 addition & 1 deletion project_consumable/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Copyright 2021-2025 - Pierre Verkest
# @author Pierre Verkest <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models
from . import report
from .hooks import set_project_ok_for_consumable_products
4 changes: 3 additions & 1 deletion project_consumable/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2021 - Pierre Verkest
# Copyright 2021-2025 - Pierre Verkest
# @author Pierre Verkest <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Project consumable",
Expand All @@ -8,6 +9,7 @@
"category": "Project Management",
"version": "17.0.1.0.0",
"license": "AGPL-3",
"maintainers": ["petrus-v"],
"depends": [
"account",
"hr_timesheet",
Expand Down
4 changes: 2 additions & 2 deletions project_consumable/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2021 - Pierre Verkest
# Copyright 2021-2025 - Pierre Verkest
# @author Pierre Verkest <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import account_analytic_line
from . import product_template
from . import project_project
from . import project_task
74 changes: 65 additions & 9 deletions project_consumable/models/account_analytic_line.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,102 @@
# Copyright 2021-2025 - Pierre Verkest
# @author Pierre Verkest <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

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


class AccountAnalyticLine(models.Model):
_inherit = "account.analytic.line"

def _timesheet_preprocess(self, vals_list):
consumable_project_id = fields.Many2one(
"project.project",
domain='[("allow_consumables", "=", True)]',
string="Project (consumable)",
)

def _consumable_preprocess_create(self, vals_list):
for vals in vals_list:
if not vals.get("name") and "consumable_project_id" in vals:
vals["name"] = "/"
return vals_list

@api.model_create_multi
def create(self, vals_list):
vals_list = self._consumable_preprocess(vals_list)
vals_list = self._consumable_preprocess_create(vals_list)

lines = super().create(vals_list)

for line, values in zip(lines, vals_list, strict=False):
if line.consumable_project_id:
line._consumable_postprocess(values)
return lines

def write(self, values):
values = self._consumable_preprocess([values])[0]
result = super().write(values)
# applied only for timesheet
self.filtered(lambda t: t.consumable_project_id)._consumable_postprocess(values)
return result

def _consumable_preprocess(self, vals_list):
"""Deduce other field values from the one given.
Overrride this to compute on the fly some field that can not be computed fields.
Override this to compute on the fly some field that can not be computed fields.
:param values: dict values for `create`or `write`.
"""
for vals in vals_list:
if all(v in vals for v in ["product_id", "project_id"]):
if all(v in vals for v in ["product_id", "consumable_project_id"]):
if "product_uom_id" not in vals:
product = (
self.env["product.product"].sudo().browse(vals["product_id"])
)
vals["product_uom_id"] = product.uom_id.id
return super()._timesheet_preprocess(vals_list)
if not vals.get("account_id") and "consumable_project_id" in vals:
account = (
self.env["project.project"]
.browse(vals["consumable_project_id"])
.analytic_account_id
)
if not account or not account.active:
raise ValidationError(
_(
"Materials must be created on a project "
"with an active analytic account."
)
)
vals["account_id"] = account.id
return vals_list

def _timesheet_postprocess_values(self, values):
def _consumable_postprocess(self, values):
sudo_self = self.sudo()
values_to_write = self._consumable_postprocess_values(values)
for consumable in sudo_self:
if values_to_write[consumable.id]:
consumable.write(values_to_write[consumable.id])
return values

def _consumable_postprocess_values(self, values):
"""Get the addionnal values to write on record
:param dict values: values for the model's fields, as a dictionary::
{'field_name': field_value, ...}
:return: a dictionary mapping each record id to its corresponding
dictionary values to write (may be empty).
"""
result = super()._timesheet_postprocess_values(values)
result = {id_: {} for id_ in self.ids}
sudo_self = self.sudo()

if any(
field_name in values
for field_name in [
"unit_amount",
"product_id",
"account_id",
"product_uom_id",
"date",
]
):
for material in sudo_self:
if material.project_id and material.product_id:
if material.consumable_project_id and material.product_id:
cost = material.product_id.standard_price or 0.0
qty = material.unit_amount
if (
Expand Down
1 change: 1 addition & 0 deletions project_consumable/models/product_template.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2021-2025 - Pierre Verkest
# @author Pierre Verkest <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models

Expand Down
96 changes: 77 additions & 19 deletions project_consumable/models/project_project.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,90 @@
# Copyright 2021-2025 Pierre Verkest
# Copyright 2021-2025 - Pierre Verkest
# @author Pierre Verkest <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

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


class Project(models.Model):
_inherit = "project.project"

timesheet_ids = fields.One2many(
domain=[("product_id", "=", None)],
company_currency_id = fields.Many2one(
string="Company Currency",
related="company_id.currency_id",
readonly=True,
)
allow_consumables = fields.Boolean(
"Consumable", default=False, help="Project allowed while collecting consumable"
)
consumable_ids = fields.One2many(
"account.analytic.line", "consumable_project_id", "Associated Consumables"
)

consumable_count = fields.Integer(
compute="_compute_consumable_count",
compute="_compute_consumable_total_price",
compute_sudo=True,
help="Number of consumable lines collected.",
)
consumable_total_price = fields.Monetary(
compute="_compute_consumable_total_price",
help="Total price of all consumables recorded in the project.",
compute_sudo=True,
currency_field="company_currency_id",
)

def _compute_consumable_count(self):
read_group = {
group["project_id"][0]: group["project_id_count"]
for group in self.env["account.analytic.line"].read_group(
[
("project_id", "in", self.ids),
("product_id", "!=", False),
],
["project_id"],
["project_id"],
)
}
@api.constrains("allow_consumables", "analytic_account_id")
def _check_allow_consumables(self):
for project in self:
project.consumable_count = read_group.get(project.id, 0)
if project.allow_consumables and not project.analytic_account_id:
raise ValidationError(
_("You cannot use consumables without an analytic account.")
)

@api.depends(
"consumable_ids",
"consumable_ids.amount",
"consumable_ids.consumable_project_id",
)
def _compute_consumable_total_price(self):
consumables_read_group = self.env["account.analytic.line"]._read_group(
[("consumable_project_id", "in", self.ids)],
["consumable_project_id"],
["consumable_project_id:count", "amount:sum"],
)
self.consumable_total_price = 0
self.consumable_count = 0
for project, count, amount_sum in consumables_read_group:
project.consumable_total_price = amount_sum
project.consumable_count = count

def action_project_consumable(self):
action = self.env["ir.actions.act_window"]._for_xml_id(
"project_consumable.consumable_action_report_by_project"
)
action["display_name"] = _("%(name)s's Materials", name=self.name)
action["domain"] = [("consumable_project_id", "in", self.ids)]
return action

def _get_stat_buttons(self):
buttons = super()._get_stat_buttons()
if not self.allow_consumables or not self.env.user.has_group(
"project.group_project_manager"
):
return buttons

buttons.append(
{
"icon": "copy",
"text": _lt("Materials"),
"number": _lt(
"%(amount)s € (%(count)s)",
amount=self.consumable_total_price,
count=self.consumable_count,
),
"action_type": "object",
"action": "action_project_consumable",
"show": True,
"sequence": 6,
}
)
return buttons
11 changes: 0 additions & 11 deletions project_consumable/models/project_task.py

This file was deleted.

2 changes: 1 addition & 1 deletion project_consumable/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
- Pierre Verkest \<<[email protected]>\>
- Pierre Verkest \<<[email protected]>\>
3 changes: 2 additions & 1 deletion project_consumable/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
This module provides a 'closed' flag on project task stages.
This module allow to collect materials/consumable linked to a
project adding account analytic lines.
1 change: 0 additions & 1 deletion project_consumable/readme/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ quantities and Unit of Mesure provided by users, analytic amount will be
computed based on product cost.

* Material & Consumable Menu
* On task tab
* Project tab

## Review consumable amount
Expand Down
2 changes: 0 additions & 2 deletions project_consumable/report/__init__.py

This file was deleted.

64 changes: 0 additions & 64 deletions project_consumable/report/hr_timesheet_attendance_report.py

This file was deleted.

Loading

0 comments on commit 0007d75

Please sign in to comment.