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 + +