Add an option to include the timesheet costs in the production costs

Closes #13304
This commit is contained in:
Sergi Almacellas Abellana
2026-04-07 16:46:21 +02:00
parent 063d384a4e
commit 2fcfc82bcf
11 changed files with 197 additions and 4 deletions
+7 -3
View File
@@ -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
@@ -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
@@ -56,4 +56,4 @@ copyright = 'COPYRIGHT'
readme = 'README.rst'
[tool.hatch.metadata.hooks.tryton.tryton-optional-dependencies]
test = []
test = ['proteus']
@@ -0,0 +1,4 @@
[
{"include_timesheet_cost": false}
,{"include_timesheet_cost": true}
]
@@ -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)
@@ -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()
@@ -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)
@@ -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
@@ -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>
+55
View File
@@ -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
@@ -9,4 +9,11 @@ this repository contains the full copyright notices and license terms. -->
<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>