diff --git a/modules/production_work/work.py b/modules/production_work/work.py
index 2496d5529f..0ecf8e5a89 100644
--- a/modules/production_work/work.py
+++ b/modules/production_work/work.py
@@ -242,12 +242,11 @@ class Work(sequence_ordered(), ModelSQL, ModelView, ChatMixin):
]
@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 @@ class Work(sequence_ordered(), ModelSQL, ModelView, ChatMixin):
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 --git a/modules/production_work_timesheet/CHANGELOG b/modules/production_work_timesheet/CHANGELOG
index 7b9b538b5d..b14754a25e 100644
--- a/modules/production_work_timesheet/CHANGELOG
+++ b/modules/production_work_timesheet/CHANGELOG
@@ -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 --git a/modules/production_work_timesheet/pyproject.toml b/modules/production_work_timesheet/pyproject.toml
index 1d508d5ff5..ae4258b0df 100644
--- a/modules/production_work_timesheet/pyproject.toml
+++ b/modules/production_work_timesheet/pyproject.toml
@@ -56,4 +56,4 @@ copyright = 'COPYRIGHT'
readme = 'README.rst'
[tool.hatch.metadata.hooks.tryton.tryton-optional-dependencies]
-test = []
+test = ['proteus']
diff --git a/modules/production_work_timesheet/tests/scenario_production_work_timesheet.json b/modules/production_work_timesheet/tests/scenario_production_work_timesheet.json
new file mode 100644
index 0000000000..d4387f132a
--- /dev/null
+++ b/modules/production_work_timesheet/tests/scenario_production_work_timesheet.json
@@ -0,0 +1,4 @@
+[
+ {"include_timesheet_cost": false}
+ ,{"include_timesheet_cost": true}
+]
diff --git a/modules/production_work_timesheet/tests/scenario_production_work_timesheet.rst b/modules/production_work_timesheet/tests/scenario_production_work_timesheet.rst
new file mode 100644
index 0000000000..75d5f09edb
--- /dev/null
+++ b/modules/production_work_timesheet/tests/scenario_production_work_timesheet.rst
@@ -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 --git a/modules/production_work_timesheet/tests/test_module.py b/modules/production_work_timesheet/tests/test_module.py
index e76d613dcf..21c44b1266 100644
--- a/modules/production_work_timesheet/tests/test_module.py
+++ b/modules/production_work_timesheet/tests/test_module.py
@@ -12,6 +12,7 @@ from trytond.tests.test_tryton import ModuleTestCase, with_transaction
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 --git a/modules/production_work_timesheet/tests/test_scenario.py b/modules/production_work_timesheet/tests/test_scenario.py
new file mode 100644
index 0000000000..dc1d23e7e5
--- /dev/null
+++ b/modules/production_work_timesheet/tests/test_scenario.py
@@ -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 --git a/modules/production_work_timesheet/tryton.cfg b/modules/production_work_timesheet/tryton.cfg
index b254246b10..db5cd334c1 100644
--- a/modules/production_work_timesheet/tryton.cfg
+++ b/modules/production_work_timesheet/tryton.cfg
@@ -5,6 +5,8 @@ depends:
production_routing
production_work
timesheet
+extras_depend:
+ timesheet_cost
xml:
work.xml
routing.xml
@@ -14,3 +16,7 @@ model:
work.Work
routing.Operation
timesheet.Work
+
+[register timesheet_cost]
+model:
+ work.WorkCenter
diff --git a/modules/production_work_timesheet/view/work_center_form.xml b/modules/production_work_timesheet/view/work_center_form.xml
new file mode 100644
index 0000000000..444bd1d1b7
--- /dev/null
+++ b/modules/production_work_timesheet/view/work_center_form.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/modules/production_work_timesheet/work.py b/modules/production_work_timesheet/work.py
index d9bd6ca7bd..755c8f253c 100644
--- a/modules/production_work_timesheet/work.py
+++ b/modules/production_work_timesheet/work.py
@@ -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 @@ class Work(metaclass=PoolMeta):
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 --git a/modules/production_work_timesheet/work.xml b/modules/production_work_timesheet/work.xml
index f21445f89d..6f754e9ade 100644
--- a/modules/production_work_timesheet/work.xml
+++ b/modules/production_work_timesheet/work.xml
@@ -9,4 +9,11 @@ this repository contains the full copyright notices and license terms. -->
work_form
+
+
+ production.work.center
+
+ work_center_form
+
+