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>

Reply via email to