Add an option to include the timesheet costs in the production costs
Closes #13304
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user