Reimplement history state to allow edit by user.

This commit refs #16115
This commit is contained in:
Sergio Morillo 2021-01-14 16:58:40 +01:00
parent 43c8ed4714
commit e855d01812
10 changed files with 485 additions and 152 deletions

View File

@ -1,15 +1,24 @@
# The COPYRIGHT file at the top level of this repository contains the full # The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms. # copyright notices and license terms.
from trytond.pool import Pool from trytond.pool import Pool
from .location import Location, LocationHistory, Combined from . import location
from . import unit_load
def register(): def register():
Pool.register( Pool.register(
Combined, location.Location,
location.LocationState,
module='stock_location_history_state', type_='model')
Pool.register(
location.Combined,
module='stock_location_history_state', type_='model', module='stock_location_history_state', type_='model',
depends=['stock_location_combined']) depends=['stock_location_combined'])
Pool.register( Pool.register(
Location, unit_load.UnitLoad,
LocationHistory, module='stock_location_history_state', type_='model',
module='stock_location_history_state', type_='model') depends=['stock_unit_load'])
Pool.register(
unit_load.UnitLoadCombined,
module='stock_location_history_state', type_='model',
depends=['stock_unit_load_location_combined'])

View File

@ -2,13 +2,17 @@
msgid "" msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n" msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:stock.location,history:" msgctxt "field:stock.location,states:"
msgid "History" msgid "History State"
msgstr "Historial" msgstr "Historial de estados"
msgctxt "field:stock.location,history_state:" msgctxt "field:stock.location,last_states:"
msgid "History state" msgid "Last History State"
msgstr "Estado histórico" msgstr "Historial de últimos estados"
msgctxt "field:stock.location,state_at:"
msgid "State at"
msgstr "Estado a fecha"
msgctxt "field:stock.location,state:" msgctxt "field:stock.location,state:"
msgid "State" msgid "State"
@ -18,58 +22,62 @@ msgctxt "field:stock.location,state_icon:"
msgid "State Icon" msgid "State Icon"
msgstr "Icono estado" msgstr "Icono estado"
msgctxt "field:stock.location.history,create_date:" msgctxt "field:stock.location.state,create_date:"
msgid "Create Date" msgid "Create Date"
msgstr "Fecha creación" msgstr "Fecha creación"
msgctxt "field:stock.location.history,create_uid:" msgctxt "field:stock.location.state,create_uid:"
msgid "Create User" msgid "Create User"
msgstr "Usuario creación" msgstr "Usuario creación"
msgctxt "field:stock.location.history,date:" msgctxt "field:stock.location.state,date:"
msgid "Change Date" msgid "Change Date"
msgstr "Fecha modificación" msgstr "Fecha modificación"
msgctxt "field:stock.location.history,id:" msgctxt "field:stock.location.state,id:"
msgid "ID" msgid "ID"
msgstr "Identificador" msgstr "Identificador"
msgctxt "field:stock.location.history,location:" msgctxt "field:stock.location.state,location:"
msgid "Location" msgid "Location"
msgstr "Ubicación" msgstr "Ubicación"
msgctxt "field:stock.location.history,rec_name:" msgctxt "field:stock.location.state,rec_name:"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
msgctxt "field:stock.location.history,state:" msgctxt "field:stock.location.state,state:"
msgid "State" msgid "State"
msgstr "Estado" msgstr "Estado"
msgctxt "field:stock.location.history,user:" msgctxt "field:stock.location.state,write_date:"
msgid "User"
msgstr "Usuario"
msgctxt "field:stock.location.history,write_date:"
msgid "Write Date" msgid "Write Date"
msgstr "Fecha modificación" msgstr "Fecha modificación"
msgctxt "field:stock.location.history,write_uid:" msgctxt "field:stock.location.state,write_uid:"
msgid "Write User" msgid "Write User"
msgstr "Usuario modificación" msgstr "Usuario modificación"
msgctxt "model:stock.location.history,name:" msgctxt "model:stock.location.state,name:"
msgid "Stock location History" msgid "Stock location History State"
msgstr "Historial ubicación" msgstr "Historial de estado de ubicación"
msgctxt "selection:stock.location,history_state:" msgctxt "selection:stock.location,state_at:"
msgid ""
msgstr ""
msgctxt "selection:stock.location,state_at:"
msgid "Off" msgid "Off"
msgstr "Parado" msgstr "Parado"
msgctxt "selection:stock.location,history_state:" msgctxt "selection:stock.location,state_at:"
msgid "On" msgid "On"
msgstr "En servicio" msgstr "En servicio"
msgctxt "selection:stock.location,state:"
msgid ""
msgstr ""
msgctxt "selection:stock.location,state:" msgctxt "selection:stock.location,state:"
msgid "Off" msgid "Off"
msgstr "Parado" msgstr "Parado"
@ -78,18 +86,14 @@ msgctxt "selection:stock.location,state:"
msgid "On" msgid "On"
msgstr "En servicio" msgstr "En servicio"
msgctxt "selection:stock.location.history,state:" msgctxt "selection:stock.location.state,state:"
msgid "Off" msgid "Off"
msgstr "Parado" msgstr "Parado"
msgctxt "selection:stock.location.history,state:" msgctxt "selection:stock.location.state,state:"
msgid "On" msgid "On"
msgstr "En servicio" msgstr "En servicio"
msgctxt "view:stock.location:"
msgid "History"
msgstr "Historial"
msgctxt "model:ir.model.button,string:location_on_button" msgctxt "model:ir.model.button,string:location_on_button"
msgid "Start" msgid "Start"
msgstr "Iniciar" msgstr "Iniciar"
@ -98,6 +102,10 @@ msgctxt "model:ir.model.button,string:location_off_button"
msgid "Stop" msgid "Stop"
msgstr "Parar" msgstr "Parar"
msgctxt "view:stock.location.history:" msgctxt "view:stock.location.state:"
msgid "Change Time" msgid "Time"
msgstr "Hora modificación" msgstr "Hora"
msgctxt "model:ir.action,name:act_location_state"
msgid "Histoy State"
msgstr "Historial de estados"

View File

@ -1,39 +1,55 @@
# The COPYRIGHT file at the top level of # The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from datetime import datetime from datetime import datetime, timedelta
from sql import Column from sql.aggregate import Max
from sql.aggregate import Min, Max
from sql.conditionals import Coalesce from sql.conditionals import Coalesce
from sql.functions import DateTrunc from sql.functions import RowNumber
from trytond.model import ModelSQL, ModelView, fields from sql import Table, Null, Window
from trytond.model import ModelSQL, ModelView, fields, Unique
from trytond.pool import PoolMeta, Pool from trytond.pool import PoolMeta, Pool
from trytond.pyson import Eval, Or from trytond.pyson import Eval, Or, If
from trytond.transaction import Transaction from trytond.transaction import Transaction
from trytond import backend from trytond import backend
from itertools import groupby
__all__ = ['Location', 'LocationHistory', 'Combined'] STATES = [
(None, ''),
STATES = [('on', 'On'), ('on', 'On'),
('off', 'Off')] ('off', 'Off')
]
class Location(metaclass=PoolMeta): class Location(metaclass=PoolMeta):
__name__ = 'stock.location' __name__ = 'stock.location'
_history = True
state = fields.Selection(STATES, 'State', state = fields.Selection(STATES, 'State', readonly=True,
readonly=True, domain=[If(Eval('type') == 'production',
('state', '!=', None), ())],
states={ states={
'readonly': ~Eval('active'), 'readonly': ~Eval('active'),
'invisible': Eval('type') != 'production'}, 'invisible': Eval('type') != 'production',
},
depends=['active', 'type']) depends=['active', 'type'])
state_icon = fields.Function( state_icon = fields.Function(
fields.Char('State Icon'), 'get_state_icon') fields.Char('State Icon'), 'get_state_icon')
history_state = fields.Function( state_at = fields.Function(
fields.Selection(STATES, 'History state'), fields.Selection(STATES, 'State at'),
'get_history_state', searcher='search_history_state') 'get_state_at', searcher='search_state_at')
history = fields.One2Many('stock.location.history', 'location', states = fields.One2Many('stock.location.state', 'location',
'History', readonly=True, loading='lazy') 'History State', loading='lazy', states={
'invisible': Eval('type') != 'production',
},
context={'create_history_state': False},
order=[('date', 'DESC')], depends=['type'])
last_states = fields.Function(
fields.One2Many('stock.location.state', 'location',
'History State', states={
'invisible': Eval('type') != 'production',
},
context={'create_history_state': False},
order=[('date', 'DESC')], depends=['type']),
'get_last_states', setter='set_last_states')
_last_states_size = 10
@classmethod @classmethod
def __setup__(cls): def __setup__(cls):
@ -66,7 +82,7 @@ class Location(metaclass=PoolMeta):
if default is None: if default is None:
default = {} default = {}
default = default.copy() default = default.copy()
default.setdefault('history', None) default.setdefault('states', None)
return super(Location, cls).copy(records, default=default) return super(Location, cls).copy(records, default=default)
@classmethod @classmethod
@ -79,7 +95,7 @@ class Location(metaclass=PoolMeta):
return None return None
@classmethod @classmethod
def get_history_state(cls, records, name=None): def get_state_at(cls, records, name=None):
values = {r.id: r.state for r in records} values = {r.id: r.state for r in records}
at_date = Transaction().context.get('state_at_date', None) at_date = Transaction().context.get('state_at_date', None)
@ -87,35 +103,38 @@ class Location(metaclass=PoolMeta):
return values return values
for record in records: for record in records:
_history = sorted(record.history, key=lambda h: h.date) for state in record.states:
for _hist in _history: if state.date <= at_date:
if _hist.date > at_date: values[record.id] = state.state
break break
values[record.id] = _hist.state
return values return values
@classmethod @classmethod
def search_history_state(cls, name, clause): def search_state_at(cls, name, clause):
State = Pool().get('stock.location.state')
at_date = Transaction().context.get('state_at_date', None) at_date = Transaction().context.get('state_at_date', None)
if not at_date: if not at_date:
return [('state', ) + tuple(clause[1:])] return [('state', ) + tuple(clause[1:])]
location_history = cls.__table_history__() location_state = State.__table__()
location_state2 = State.__table__()
Operator = fields.SQL_OPERATORS[clause[1]] Operator = fields.SQL_OPERATORS[clause[1]]
columns = [location_history.id.as_('location'), columns = [
location_history.state.as_('state'), location_state.location,
Max(Coalesce(location_history.write_date, Max(location_state.id).as_('state_id'),
location_history.create_date)).as_('date')] Max(location_state.date).as_('date'),
]
# Gets state of allowed max date # Gets state of allowed max date
query = location_history.select( query = location_state.select(
*columns, *columns,
where=(Coalesce(location_history.write_date, where=(location_state.date <= at_date),
location_history.create_date) <= at_date), group_by=[location_state.location])
group_by=[location_history.id, location_history.state]) query = query.join(location_state2, condition=(
query = query.select( query.state_id == location_state2.id)
query.location, ).select(
where=(Operator(query.state, clause[2]))) query.location,
where=(Operator(location_state2.state, clause[2])))
return [('id', 'in', query)] return [('id', 'in', query)]
@classmethod @classmethod
@ -162,55 +181,261 @@ class Location(metaclass=PoolMeta):
('//page[@id="history"]', 'states', { ('//page[@id="history"]', 'states', {
'invisible': Eval('type') != 'production'})] 'invisible': Eval('type') != 'production'})]
@classmethod
def get_last_states(cls, records, name):
State = Pool().get('stock.location.state')
location_state = State.__table__()
cursor = Transaction().connection.cursor()
class LocationHistory(ModelSQL, ModelView): # Gets last states
"""Stock location History""" query = location_state.select(
__name__ = 'stock.location.history' RowNumber(window=Window(partition=[location_state.location])
).as_('counter'),
location_state.location,
location_state.id,
order_by=(location_state.location, location_state.date.desc),
)
cursor.execute(*query.select(
query.location,
query.id,
where=(query.counter <= cls._last_states_size))
)
values = cursor.fetchall()
res = {r.id: [] for r in records}
for location_id, state_ids in groupby(values, key=lambda v: v[0]):
res[location_id] = [s[1] for s in state_ids]
return res
date_ = fields.DateTime('Change Date') @classmethod
date = fields.Function(fields.DateTime('Change Date'), 'get_date') def set_last_states(cls, records, name, value):
location = fields.Many2One('stock.location', 'Location') with Transaction().set_context(create_history_state=False):
user = fields.Many2One('res.user', 'User') cls.write(records, {
state = fields.Selection(STATES, 'State') 'states': value,
})
@classmethod
def create(cls, vlist):
records = super().create(vlist)
states = {}
for record in records:
if record.state:
states.setdefault(record.state, []).append(record)
for state, stated_records in states.items():
cls.create_history_state(stated_records, state)
return cls.browse(records)
@classmethod
def write(cls, *args):
actions = iter(args)
args = []
states = {}
for records, values in zip(actions, actions):
if values.get('state', None) and \
Transaction().context.get('create_history_state', True):
states.setdefault(values['state'], []).extend(records)
args.extend((records, values))
super().write(*args)
for state, stated_records in states.items():
cls.create_history_state(stated_records, state)
@classmethod
def create_history_state(cls, records, state):
State = Pool().get('stock.location.state')
if not state:
return
if not Transaction().context.get('create_history_state', True):
# avoid create another record when editing history
return
values = []
for record in records:
values.append({
'location': record.id,
'state': state,
'date': datetime.now()
})
State.create(values)
@classmethod
def set_current_state(cls, records):
records = cls.browse(records)
to_update = {}
for record in records:
if record.states and record.state != record.states[0].state:
to_update.setdefault(record.states[0].state, []).append(record)
if to_update:
changes = []
for k, v in to_update.items():
changes.extend([v, {'state': k}])
with Transaction().set_context(create_history_state=False):
# do not use save due to context is lost
cls.write(*changes)
def get_state_time(self, from_date, to_date, state):
# get first state outside from_date
State = Pool().get('stock.location.state')
first_state = State.search([
('location', '=', self.id),
('date', '<=', from_date)],
order=[('date', 'DESC')],
limit=1)
history_states = State.search([
('location', '=', self.id),
('date', '>=', from_date),
('date', '<=', to_date)],
order=[('date', 'ASC')])
result = timedelta(0)
if first_state:
first_state, = first_state
elif history_states:
first_state = history_states[0]
else:
return None
last_date = first_state.date
last_state = first_state.state
for hstate in history_states:
if last_state == state and last_state != hstate.state:
result += (hstate.date - last_date)
last_date = hstate.date
last_state = hstate.state
if last_state == state and last_date < to_date:
result += (to_date - last_date)
return result - timedelta(microseconds=result.microseconds)
def get_state_time_on(self, from_date, to_date):
return self.get_state_time(from_date, to_date, 'on')
def get_state_time_off(self, from_date, to_date):
return self.get_state_time(from_date, to_date, 'off')
class LocationState(ModelSQL, ModelView):
"""Stock location History State"""
__name__ = 'stock.location.state'
date = fields.DateTime('Date', required=True)
location = fields.Many2One('stock.location', 'Location', select=True,
required=True, ondelete='CASCADE')
state = fields.Selection([
('on', 'On'),
('off', 'Off')], 'State', required=True)
@classmethod @classmethod
def __setup__(cls): def __setup__(cls):
super(LocationHistory, cls).__setup__() super().__setup__()
cls._order.insert(0, ('date_', 'DESC')) cls._order.insert(0, ('date', 'DESC'))
t = cls.__table__()
cls._sql_constraints += [
('date_location_uniq', Unique(t, t.date, t.location),
'Combination of Date and Location must be unique.'),
]
@classmethod @classmethod
def table_query(cls): def __register__(cls, module_name):
_Location = Pool().get('stock.location') TableHandler = backend.get('TableHandler')
location_history = _Location.__table_history__() Location = Pool().get('stock.location')
columns = [ cursor = Transaction().connection.cursor()
Min(Column(location_history, '__id')).as_('id'), table = cls.__table__()
location_history.id.as_('location'), location = Location.__table__()
Min(Coalesce(location_history.write_date,
location_history.create_date)).as_('date_'),
Coalesce(location_history.write_uid,
location_history.create_uid).as_('user'),
]
group_by = [
location_history.id,
Coalesce(location_history.write_uid,
location_history.create_uid),
]
for name, field in cls._fields.items():
if name in ('id', 'location', 'date_', 'date', 'user'):
continue
if hasattr(field, 'set'):
continue
column = Column(location_history, name)
columns.append(column.as_(name))
group_by.append(column)
return location_history.select(*columns, group_by=group_by) super().__register__(module_name)
def get_date(self, name): cursor.execute(*table.select(table.id, limit=1))
_date = self.date_ if TableHandler.table_exist('stock_location__history') and \
if not isinstance(_date, datetime): not cursor.fetchone():
_date = datetime.strptime(_date, '%Y-%m-%d %H:%M:%S.%f') history = Table('stock_location__history')
return _date history2 = Table('stock_location__history')
query = history.join(location, condition=(
location.id == history.id)
).select(
history.id,
Max(Coalesce(history.write_date, history.create_date)
).as_('date'),
where=(
(history.state != Null) &
(location.type == 'production')
),
group_by=history.id
)
cursor.execute(*table.insert([
table.create_uid,
table.write_uid,
table.create_date,
table.write_date,
table.location,
table.date,
table.state
],
history2.join(query, condition=(
(history2.id == query.id) &
(Coalesce(history2.write_date, history2.create_date
) == query.date))
).select(
history2.create_uid,
history2.write_uid,
history2.create_date,
history2.write_date,
history2.id,
query.date,
history2.state))
)
@classmethod
def default_date(cls):
return datetime.now()
@fields.depends('location', '_parent_location.state')
def on_change_location(self):
mapping = {
'on': 'off',
'off': 'on'
}
if self.location and self.location.state:
self.state = mapping[self.location.state]
@classmethod
def create(cls, vlist):
Location = Pool().get('stock.location')
records = super().create(vlist)
locations = set([r.location for r in records])
if locations:
Location.set_current_state(locations)
return records
@classmethod
def write(cls, *args):
Location = Pool().get('stock.location')
actions = iter(args)
args = []
locations = []
for records, values in zip(actions, actions):
locations.extend([r.location for r in records])
args.extend((records, values))
super().write(*args)
if locations:
locations = set(locations)
Location.set_current_state(locations)
@classmethod
def delete(cls, records):
Location = Pool().get('stock.location')
locations = set([r.location for r in records])
super().delete(records)
if locations:
Location.set_current_state(locations)
class Combined(metaclass=PoolMeta): class Combined(metaclass=PoolMeta):

View File

@ -14,26 +14,44 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">location_form</field> <field name="name">location_form</field>
<field name="inherit" ref="stock.location_view_form"/> <field name="inherit" ref="stock.location_view_form"/>
</record> </record>
<!-- Location history -->
<record model="ir.ui.view" id="location_history_view_tree"> <!-- Location state -->
<field name="model">stock.location.history</field> <record model="ir.ui.view" id="location_state_view_tree">
<field name="model">stock.location.state</field>
<field name="type">tree</field> <field name="type">tree</field>
<field name="name">location_history_tree</field> <field name="name">location_state_tree</field>
</record> </record>
<record model="ir.model.access" id="access_location_history"> <record model="ir.model.access" id="access_location_state">
<field name="model" search="[('model', '=', 'stock.location.history')]"/> <field name="model" search="[('model', '=', 'stock.location.state')]"/>
<field name="perm_read" eval="False"/> <field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/> <field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/> <field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/> <field name="perm_delete" eval="False"/>
</record> </record>
<record model="ir.model.access" id="access_location_history_stock"> <record model="ir.model.access" id="access_location_state_stock">
<field name="model" search="[('model', '=', 'stock.location.history')]"/> <field name="model" search="[('model', '=', 'stock.location.state')]"/>
<field name="group" ref="stock.group_stock"/> <field name="group" ref="stock.group_stock_admin"/>
<field name="perm_read" eval="True"/> <field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/> <field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/> <field name="perm_create" eval="True"/>
<field name="perm_delete" eval="False"/> <field name="perm_delete" eval="True"/>
</record>
<!-- Form relate -->
<record model="ir.action.act_window" id="act_location_state">
<field name="name">Histoy State</field>
<field name="res_model">stock.location.state</field>
<field name="domain" eval="[If(Eval('active_ids', []) == [Eval('active_id')], ('location', '=', Eval('active_id')), ('location', 'in', Eval('active_ids')))]" pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_location_state_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="location_state_view_tree"/>
<field name="act_window" ref="act_location_state"/>
</record>
<record model="ir.action.keyword" id="act_open_location_states_keyword1">
<field name="keyword">form_relate</field>
<field name="model">stock.location,-1</field>
<field name="action" ref="act_location_state"/>
</record> </record>
<!-- Buttons --> <!-- Buttons -->

View File

@ -2,6 +2,7 @@
# copyright notices and license terms. # copyright notices and license terms.
import unittest import unittest
import time import time
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
import trytond.tests.test_tryton import trytond.tests.test_tryton
from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.tests.test_tryton import ModuleTestCase, with_transaction
@ -17,14 +18,18 @@ class LocationStateHistoryTestCase(ModuleTestCase):
def test_change_state(self): def test_change_state(self):
"""Change location state""" """Change location state"""
Location = Pool().get('stock.location') Location = Pool().get('stock.location')
State = Pool().get('stock.location.state')
transaction = Transaction() transaction = Transaction()
production, = Location.create([ production, = Location.create([{
{'code': 'PL', 'code': 'PL',
'name': 'Production location', 'name': 'Production location',
'type': 'production'}]) 'type': 'production'
}])
self.assertEqual(production.state, 'on') self.assertEqual(production.state, 'on')
transaction.commit() transaction.commit()
from_date = datetime.now().replace(microsecond=0
) - relativedelta(seconds=5)
time.sleep(2) time.sleep(2)
Location.off([production]) Location.off([production])
@ -32,35 +37,70 @@ class LocationStateHistoryTestCase(ModuleTestCase):
transaction.commit() transaction.commit()
time.sleep(2) time.sleep(2)
child, = Location.create([ child, = Location.create([{
{'code': 'PL2', 'code': 'PL2',
'name': 'Prod. location 2', 'name': 'Prod. location 2',
'type': 'production', 'type': 'production',
'parent': production.id, 'parent': production.id,
'state': 'off'}]) 'state': 'off'
}])
time.sleep(1)
Location.on([production]) Location.on([production])
transaction.commit() transaction.commit()
self.assert_(production.state == 'on' and child.state == 'on') self.assertTrue(production.state == 'on' and child.state == 'on')
self.assertEqual(len(production.history), 3) self.assertEqual(len(production.states), 3)
values = {} values = {}
for line in production.history: for line in production.states:
values.setdefault(line.state, 0) values.setdefault(line.state, 0)
values[line.state] += 1 values[line.state] += 1
self.assertEqual(values, {'on': 2, 'off': 1}) self.assertEqual(values, {'on': 2, 'off': 1})
self.assertEqual(production.history_state, 'on') self.assertEqual(production.state_at, 'on')
at_date, = [h.date for h in production.history if h.state == 'off'] at_date, = [h.date for h in production.states if h.state == 'off']
at_date += relativedelta(seconds=0.5) at_date += relativedelta(seconds=0.5)
with Transaction().set_context(state_at_date=at_date): with Transaction().set_context(state_at_date=at_date):
other_loc, = Location.search([ other_loc, = Location.search([
('code', '=', 'PL'), ('code', '=', 'PL'),
('history_state', '=', 'off')]) ('state_at', '=', 'off')])
self.assertEqual(production.id, other_loc.id) self.assertEqual(production.id, other_loc.id)
self.assertEqual(production.get_history_state( self.assertEqual(production.get_state_at(
[production])[production.id], 'off') [production])[production.id], 'off')
self.assertEqual(Location.search([
('code', '=', 'PL'),
('state_at', '=', 'on')]) or [], [])
# check time in on state
self.assertEqual(production.get_state_time_on(from_date,
from_date + relativedelta(seconds=9)), timedelta(seconds=2))
self.assertEqual(production.get_state_time_on(
from_date + relativedelta(seconds=8),
from_date + relativedelta(seconds=12)), timedelta(seconds=2))
self.assertEqual(
production.get_state_time_on(
from_date + relativedelta(seconds=5),
from_date + relativedelta(seconds=12)
) +
production.get_state_time_off(
from_date + relativedelta(seconds=5),
from_date + relativedelta(seconds=12)
), timedelta(seconds=7))
# check update current state
self.assertEqual(len(production.states), 3)
State.delete([production.states[0]])
transaction.commit()
self.assertEqual(production.state, 'off')
self.assertEqual(len(production.states), 2)
Location.write([production], {
'last_states': [('delete', [production.states[-1]])]
})
transaction.commit()
production = Location(production.id)
self.assertEqual(production.state, 'off')
self.assertEqual(len(production.states), 1)
def suite(): def suite():

View File

@ -7,6 +7,7 @@ depends:
extras_depend: extras_depend:
stock_location_combined stock_location_combined
stock_unit_load
xml: xml:
location.xml location.xml

32
unit_load.py Normal file
View File

@ -0,0 +1,32 @@
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from trytond.pool import PoolMeta
class UnitLoad(metaclass=PoolMeta):
__name__ = 'stock.unit_load'
def get_production_time(self, name=None):
result = None
if self.production_location:
result = self.production_location.get_state_time_on(
self.start_date, self.end_date)
if result is not None:
return result
return super().get_production_time(name)
class UnitLoadCombined(metaclass=PoolMeta):
__name__ = 'stock.unit_load'
def get_production_time(self, name=None):
if self.location_combined and self.production_locations:
times = []
for location in self.production_locations:
time_ = location.get_state_time_on(
self.start_date, self.end_date)
if time_ is not None:
times.append(time_)
if times:
return min(times)
return super().get_production_time(name)

View File

@ -3,8 +3,8 @@
this repository contains the full copyright notices and license terms. --> this repository contains the full copyright notices and license terms. -->
<data> <data>
<xpath expr="/form/notebook" position="inside"> <xpath expr="/form/notebook" position="inside">
<page id="history" string="History"> <page name="states">
<field name="history" colspan="4"/> <field name="last_states" colspan="4"/>
</page> </page>
</xpath> </xpath>
<xpath expr="/form" position="inside"> <xpath expr="/form" position="inside">

View File

@ -1,9 +1,9 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full <!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. --> copyright notices and license terms. -->
<tree> <tree editable="top">
<field name="location"/>
<field name="date" widget="date"/> <field name="date" widget="date"/>
<field name="date" widget="time" string="Change Time"/> <field name="date" widget="time" string="Time"/>
<field name="user"/>
<field name="state"/> <field name="state"/>
</tree> </tree>

View File

@ -4,7 +4,7 @@ this repository contains the full copyright notices and license terms. -->
<data> <data>
<xpath expr="/tree" position="inside"> <xpath expr="/tree" position="inside">
<field name="state" icon="state_icon"/> <field name="state" icon="state_icon"/>
<button string="Start" name="on"/> <button name="on"/>
<button string="Stop" name="off"/> <button name="off"/>
</xpath> </xpath>
</data> </data>