details: https://code.tryton.org/tryton/commit/f2642fe60c20
branch: default
user: Sergi Almacellas Abellana <[email protected]>
date: Tue Apr 07 16:46:21 2026 +0200
description:
Add an option to include the timesheet costs in the production costs
Closes #13304
diffstat:
modules/production_work/work.py
| 10 +-
modules/production_work_timesheet/CHANGELOG
| 1 +
modules/production_work_timesheet/pyproject.toml
| 2 +-
modules/production_work_timesheet/tests/scenario_production_work_timesheet.json
| 4 +
modules/production_work_timesheet/tests/scenario_production_work_timesheet.rst
| 98 ++++++++++
modules/production_work_timesheet/tests/test_module.py
| 1 +
modules/production_work_timesheet/tests/test_scenario.py
| 8 +
modules/production_work_timesheet/tryton.cfg
| 6 +
modules/production_work_timesheet/view/work_center_form.xml
| 9 +
modules/production_work_timesheet/work.py
| 55 +++++
modules/production_work_timesheet/work.xml
| 7 +
11 files changed, 197 insertions(+), 4 deletions(-)
diffs (304 lines):
diff -r 8a8178270374 -r f2642fe60c20 modules/production_work/work.py
--- a/modules/production_work/work.py Tue Apr 07 17:40:41 2026 +0200
+++ b/modules/production_work/work.py Tue Apr 07 16:46:21 2026 +0200
@@ -242,12 +242,11 @@
]
@classmethod
- def get_cost(cls, works, name):
+ def _get_cost(cls, works):
pool = Pool()
Cycle = pool.get('production.work.cycle')
cycle = Cycle.__table__()
cursor = Transaction().connection.cursor()
- costs = defaultdict(Decimal)
query = cycle.select(
cycle.work, Sum(Coalesce(cycle.cost, 0)).as_('cost'),
@@ -257,7 +256,12 @@
if backend.name == 'sqlite':
sqlite_apply_types(query, [None, 'NUMERIC'])
cursor.execute(*query)
- for work_id, cost in cursor:
+ return defaultdict(Decimal, cursor)
+
+ @classmethod
+ def get_cost(cls, works, name):
+ costs = defaultdict(Decimal)
+ for work_id, cost in cls._get_cost(works).items():
costs[work_id] = round_price(cost)
return costs
diff -r 8a8178270374 -r f2642fe60c20 modules/production_work_timesheet/CHANGELOG
--- a/modules/production_work_timesheet/CHANGELOG Tue Apr 07 17:40:41
2026 +0200
+++ b/modules/production_work_timesheet/CHANGELOG Tue Apr 07 16:46:21
2026 +0200
@@ -1,3 +1,4 @@
+* Add an option to include the timesheet costs in the production costs
* Add support for Python 3.14
* Remove support for Python 3.9
diff -r 8a8178270374 -r f2642fe60c20
modules/production_work_timesheet/pyproject.toml
--- a/modules/production_work_timesheet/pyproject.toml Tue Apr 07 17:40:41
2026 +0200
+++ b/modules/production_work_timesheet/pyproject.toml Tue Apr 07 16:46:21
2026 +0200
@@ -56,4 +56,4 @@
readme = 'README.rst'
[tool.hatch.metadata.hooks.tryton.tryton-optional-dependencies]
-test = []
+test = ['proteus']
diff -r 8a8178270374 -r f2642fe60c20
modules/production_work_timesheet/tests/scenario_production_work_timesheet.json
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++
b/modules/production_work_timesheet/tests/scenario_production_work_timesheet.json
Tue Apr 07 16:46:21 2026 +0200
@@ -0,0 +1,4 @@
+[
+ {"include_timesheet_cost": false}
+ ,{"include_timesheet_cost": true}
+]
diff -r 8a8178270374 -r f2642fe60c20
modules/production_work_timesheet/tests/scenario_production_work_timesheet.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++
b/modules/production_work_timesheet/tests/scenario_production_work_timesheet.rst
Tue Apr 07 16:46:21 2026 +0200
@@ -0,0 +1,98 @@
+==================================
+Production Work Timesheet Scenario
+==================================
+
+Imports::
+
+ >>> import datetime as dt
+ >>> from decimal import Decimal
+
+ >>> from proteus import Model
+ >>> from trytond.modules.company.tests.tools import create_company
+ >>> from trytond.tests.tools import activate_modules, assertEqual
+
+ >>> include_timesheet_cost = globals().get('include_timesheet_cost', False)
+ >>> cost = Decimal('80.0000') if include_timesheet_cost else
Decimal('10.0000')
+
+Activate modules::
+
+ >>> config = activate_modules(
+ ... ['production_work_timesheet', 'timesheet_cost'])
+
+ >>> Employee = Model.get('company.employee')
+ >>> Operation = Model.get('production.routing.operation')
+ >>> Party = Model.get('party.party')
+ >>> Production = Model.get('production')
+ >>> WorkCenter = Model.get('production.work.center')
+
+Create company::
+
+ >>> _ = create_company()
+
+Create employee::
+
+ >>> employee_party = Party(name="Employee")
+ >>> employee_party.save()
+ >>> employee = Employee(party=employee_party)
+ >>> cost_price = employee.cost_prices.new()
+ >>> cost_price.cost_price = Decimal('35.00')
+ >>> employee.save()
+
+Create work center and operation::
+
+ >>> work_center = WorkCenter(name="Work Center")
+ >>> work_center.cost_price = Decimal('10')
+ >>> work_center.cost_method = 'cycle'
+ >>> work_center.include_timesheet_cost = include_timesheet_cost
+ >>> work_center.save()
+
+ >>> operation = Operation(name="Operation")
+ >>> operation.timesheet_available = True
+ >>> operation.save()
+
+Make a production::
+
+ >>> production = Production()
+ >>> work = production.works.new()
+ >>> work.operation = operation
+ >>> work.work_center = work_center
+ >>> production.click('wait')
+ >>> production.state
+ 'waiting'
+ >>> production.cost
+ Decimal('0.0000')
+
+Run the production::
+
+ >>> production.click('assign_try')
+ >>> production.click('run')
+ >>> production.state
+ 'running'
+ >>> work, = production.works
+ >>> work.click('start')
+ >>> work_line = work.timesheet_lines.new()
+ >>> work_line.employee = employee
+ >>> work_line.duration = dt.timedelta(hours=2)
+ >>> work.click('stop')
+ >>> work.state
+ 'finished'
+
+Check production cost::
+
+ >>> production.reload()
+ >>> assertEqual(production.cost, cost)
+ >>> work, = production.works
+ >>> assertEqual(work.cost, cost)
+
+Do the production::
+
+ >>> production.click('do')
+ >>> production.state
+ 'done'
+
+Check production cost::
+
+ >>> production.reload()
+ >>> assertEqual(production.cost, cost)
+ >>> work, = production.works
+ >>> assertEqual(work.cost, cost)
diff -r 8a8178270374 -r f2642fe60c20
modules/production_work_timesheet/tests/test_module.py
--- a/modules/production_work_timesheet/tests/test_module.py Tue Apr 07
17:40:41 2026 +0200
+++ b/modules/production_work_timesheet/tests/test_module.py Tue Apr 07
16:46:21 2026 +0200
@@ -12,6 +12,7 @@
class ProductionWorkTimesheetTestCase(CompanyTestMixin, ModuleTestCase):
'Test Production Work Timesheet module'
module = 'production_work_timesheet'
+ extras = ['timesheet_cost']
def create_work(self, production_state='draft'):
pool = Pool()
diff -r 8a8178270374 -r f2642fe60c20
modules/production_work_timesheet/tests/test_scenario.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/production_work_timesheet/tests/test_scenario.py Tue Apr 07
16:46:21 2026 +0200
@@ -0,0 +1,8 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+from trytond.tests.test_tryton import load_doc_tests
+
+
+def load_tests(*args, **kwargs):
+ return load_doc_tests(__name__, __file__, *args, **kwargs)
diff -r 8a8178270374 -r f2642fe60c20
modules/production_work_timesheet/tryton.cfg
--- a/modules/production_work_timesheet/tryton.cfg Tue Apr 07 17:40:41
2026 +0200
+++ b/modules/production_work_timesheet/tryton.cfg Tue Apr 07 16:46:21
2026 +0200
@@ -5,6 +5,8 @@
production_routing
production_work
timesheet
+extras_depend:
+ timesheet_cost
xml:
work.xml
routing.xml
@@ -14,3 +16,7 @@
work.Work
routing.Operation
timesheet.Work
+
+[register timesheet_cost]
+model:
+ work.WorkCenter
diff -r 8a8178270374 -r f2642fe60c20
modules/production_work_timesheet/view/work_center_form.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/production_work_timesheet/view/work_center_form.xml Tue Apr
07 16:46:21 2026 +0200
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+ <xpath expr="/form/field[@name='cost_method']" position="after">
+ <label name="include_timesheet_cost"/>
+ <field name="include_timesheet_cost"/>
+ </xpath>
+</data>
diff -r 8a8178270374 -r f2642fe60c20 modules/production_work_timesheet/work.py
--- a/modules/production_work_timesheet/work.py Tue Apr 07 17:40:41 2026 +0200
+++ b/modules/production_work_timesheet/work.py Tue Apr 07 16:46:21 2026 +0200
@@ -2,12 +2,31 @@
# this repository contains the full copyright notices and license terms.
from collections import defaultdict
+from sql import Literal
+from sql.aggregate import Sum
+from sql.conditionals import Coalesce
+from sql.functions import Extract
+
+from trytond import backend
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
+from trytond.tools import sqlite_apply_types
from trytond.transaction import Transaction
+class WorkCenter(metaclass=PoolMeta):
+ __name__ = 'production.work.center'
+
+ include_timesheet_cost = fields.Boolean(
+ "Include Timesheet Cost",
+ help="Check to add the timesheet cost to the work cost.")
+
+ @classmethod
+ def default_include_timesheet_cost(cls):
+ return False
+
+
class Work(metaclass=PoolMeta):
__name__ = 'production.work'
@@ -110,3 +129,39 @@
Timesheet.write(timesheets, {
'timesheet_end_date': date,
})
+
+ @classmethod
+ def _get_cost(cls, works):
+ pool = Pool()
+ Line = pool.get('timesheet.line')
+ TimesheetWork = pool.get('timesheet.work')
+ Work = pool.get('production.work')
+ WorkCenter = pool.get('production.work.center')
+ line = Line.__table__()
+ t_work = TimesheetWork.__table__()
+ work = Work.__table__()
+ center = WorkCenter.__table__()
+
+ cursor = Transaction().connection.cursor()
+
+ costs = super()._get_cost(works)
+
+ cost = line.cost_price * Extract('EPOCH', line.duration) / (60 * 60)
+ work_origin = TimesheetWork.origin.sql_id(t_work.origin, TimesheetWork)
+ query = line.join(
+ t_work, condition=(t_work.id == line.work)).join(
+ work, condition=(work.id == work_origin)).join(
+ center, condition=(
+ (center.id == work.work_center)
+ & (center.include_timesheet_cost == Literal(True)))
+ ).select(work.id, Sum(Coalesce(cost, 0)).as_('cost'),
+ where=(
+ fields.SQL_OPERATORS['in'](work_origin, map(int, works))
+ & (t_work.origin.like(cls.__name__ + ',%'))),
+ group_by=work.id)
+ if backend.name == 'sqlite':
+ sqlite_apply_types(query, [None, 'NUMERIC'])
+ cursor.execute(*query)
+ for work_id, cost in cursor:
+ costs[work_id] += cost
+ return costs
diff -r 8a8178270374 -r f2642fe60c20 modules/production_work_timesheet/work.xml
--- a/modules/production_work_timesheet/work.xml Tue Apr 07 17:40:41
2026 +0200
+++ b/modules/production_work_timesheet/work.xml Tue Apr 07 16:46:21
2026 +0200
@@ -9,4 +9,11 @@
<field name="name">work_form</field>
</record>
</data>
+ <data depends="timesheet_cost">
+ <record model="ir.ui.view" id="work_center_view_form">
+ <field name="model">production.work.center</field>
+ <field name="inherit" ref="production_work.work_center_view_form"/>
+ <field name="name">work_center_form</field>
+ </record>
+ </data>
</tryton>