Reimplement history state to allow edit by user.
This commit refs #16115
This commit is contained in:
parent
43c8ed4714
commit
e855d01812
19
__init__.py
19
__init__.py
|
@ -1,15 +1,24 @@
|
|||
# The COPYRIGHT file at the top level of this repository contains the full
|
||||
# copyright notices and license terms.
|
||||
from trytond.pool import Pool
|
||||
from .location import Location, LocationHistory, Combined
|
||||
from . import location
|
||||
from . import unit_load
|
||||
|
||||
|
||||
def 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',
|
||||
depends=['stock_location_combined'])
|
||||
Pool.register(
|
||||
Location,
|
||||
LocationHistory,
|
||||
module='stock_location_history_state', type_='model')
|
||||
unit_load.UnitLoad,
|
||||
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'])
|
||||
|
|
74
locale/es.po
74
locale/es.po
|
@ -2,13 +2,17 @@
|
|||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=utf-8\n"
|
||||
|
||||
msgctxt "field:stock.location,history:"
|
||||
msgid "History"
|
||||
msgstr "Historial"
|
||||
msgctxt "field:stock.location,states:"
|
||||
msgid "History State"
|
||||
msgstr "Historial de estados"
|
||||
|
||||
msgctxt "field:stock.location,history_state:"
|
||||
msgid "History state"
|
||||
msgstr "Estado histórico"
|
||||
msgctxt "field:stock.location,last_states:"
|
||||
msgid "Last History State"
|
||||
msgstr "Historial de últimos estados"
|
||||
|
||||
msgctxt "field:stock.location,state_at:"
|
||||
msgid "State at"
|
||||
msgstr "Estado a fecha"
|
||||
|
||||
msgctxt "field:stock.location,state:"
|
||||
msgid "State"
|
||||
|
@ -18,58 +22,62 @@ msgctxt "field:stock.location,state_icon:"
|
|||
msgid "State Icon"
|
||||
msgstr "Icono estado"
|
||||
|
||||
msgctxt "field:stock.location.history,create_date:"
|
||||
msgctxt "field:stock.location.state,create_date:"
|
||||
msgid "Create Date"
|
||||
msgstr "Fecha creación"
|
||||
|
||||
msgctxt "field:stock.location.history,create_uid:"
|
||||
msgctxt "field:stock.location.state,create_uid:"
|
||||
msgid "Create User"
|
||||
msgstr "Usuario creación"
|
||||
|
||||
msgctxt "field:stock.location.history,date:"
|
||||
msgctxt "field:stock.location.state,date:"
|
||||
msgid "Change Date"
|
||||
msgstr "Fecha modificación"
|
||||
|
||||
msgctxt "field:stock.location.history,id:"
|
||||
msgctxt "field:stock.location.state,id:"
|
||||
msgid "ID"
|
||||
msgstr "Identificador"
|
||||
|
||||
msgctxt "field:stock.location.history,location:"
|
||||
msgctxt "field:stock.location.state,location:"
|
||||
msgid "Location"
|
||||
msgstr "Ubicación"
|
||||
|
||||
msgctxt "field:stock.location.history,rec_name:"
|
||||
msgctxt "field:stock.location.state,rec_name:"
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
msgctxt "field:stock.location.history,state:"
|
||||
msgctxt "field:stock.location.state,state:"
|
||||
msgid "State"
|
||||
msgstr "Estado"
|
||||
|
||||
msgctxt "field:stock.location.history,user:"
|
||||
msgid "User"
|
||||
msgstr "Usuario"
|
||||
|
||||
msgctxt "field:stock.location.history,write_date:"
|
||||
msgctxt "field:stock.location.state,write_date:"
|
||||
msgid "Write Date"
|
||||
msgstr "Fecha modificación"
|
||||
|
||||
msgctxt "field:stock.location.history,write_uid:"
|
||||
msgctxt "field:stock.location.state,write_uid:"
|
||||
msgid "Write User"
|
||||
msgstr "Usuario modificación"
|
||||
|
||||
msgctxt "model:stock.location.history,name:"
|
||||
msgid "Stock location History"
|
||||
msgstr "Historial ubicación"
|
||||
msgctxt "model:stock.location.state,name:"
|
||||
msgid "Stock location History State"
|
||||
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"
|
||||
msgstr "Parado"
|
||||
|
||||
msgctxt "selection:stock.location,history_state:"
|
||||
msgctxt "selection:stock.location,state_at:"
|
||||
msgid "On"
|
||||
msgstr "En servicio"
|
||||
|
||||
msgctxt "selection:stock.location,state:"
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
msgctxt "selection:stock.location,state:"
|
||||
msgid "Off"
|
||||
msgstr "Parado"
|
||||
|
@ -78,18 +86,14 @@ msgctxt "selection:stock.location,state:"
|
|||
msgid "On"
|
||||
msgstr "En servicio"
|
||||
|
||||
msgctxt "selection:stock.location.history,state:"
|
||||
msgctxt "selection:stock.location.state,state:"
|
||||
msgid "Off"
|
||||
msgstr "Parado"
|
||||
|
||||
msgctxt "selection:stock.location.history,state:"
|
||||
msgctxt "selection:stock.location.state,state:"
|
||||
msgid "On"
|
||||
msgstr "En servicio"
|
||||
|
||||
msgctxt "view:stock.location:"
|
||||
msgid "History"
|
||||
msgstr "Historial"
|
||||
|
||||
msgctxt "model:ir.model.button,string:location_on_button"
|
||||
msgid "Start"
|
||||
msgstr "Iniciar"
|
||||
|
@ -98,6 +102,10 @@ msgctxt "model:ir.model.button,string:location_off_button"
|
|||
msgid "Stop"
|
||||
msgstr "Parar"
|
||||
|
||||
msgctxt "view:stock.location.history:"
|
||||
msgid "Change Time"
|
||||
msgstr "Hora modificación"
|
||||
msgctxt "view:stock.location.state:"
|
||||
msgid "Time"
|
||||
msgstr "Hora"
|
||||
|
||||
msgctxt "model:ir.action,name:act_location_state"
|
||||
msgid "Histoy State"
|
||||
msgstr "Historial de estados"
|
381
location.py
381
location.py
|
@ -1,39 +1,55 @@
|
|||
# The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from datetime import datetime
|
||||
from sql import Column
|
||||
from sql.aggregate import Min, Max
|
||||
from datetime import datetime, timedelta
|
||||
from sql.aggregate import Max
|
||||
from sql.conditionals import Coalesce
|
||||
from sql.functions import DateTrunc
|
||||
from trytond.model import ModelSQL, ModelView, fields
|
||||
from sql.functions import RowNumber
|
||||
from sql import Table, Null, Window
|
||||
from trytond.model import ModelSQL, ModelView, fields, Unique
|
||||
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 import backend
|
||||
from itertools import groupby
|
||||
|
||||
__all__ = ['Location', 'LocationHistory', 'Combined']
|
||||
|
||||
STATES = [('on', 'On'),
|
||||
('off', 'Off')]
|
||||
STATES = [
|
||||
(None, ''),
|
||||
('on', 'On'),
|
||||
('off', 'Off')
|
||||
]
|
||||
|
||||
|
||||
class Location(metaclass=PoolMeta):
|
||||
__name__ = 'stock.location'
|
||||
_history = True
|
||||
|
||||
state = fields.Selection(STATES, 'State',
|
||||
readonly=True,
|
||||
state = fields.Selection(STATES, 'State', readonly=True,
|
||||
domain=[If(Eval('type') == 'production',
|
||||
('state', '!=', None), ())],
|
||||
states={
|
||||
'readonly': ~Eval('active'),
|
||||
'invisible': Eval('type') != 'production'},
|
||||
'invisible': Eval('type') != 'production',
|
||||
},
|
||||
depends=['active', 'type'])
|
||||
state_icon = fields.Function(
|
||||
fields.Char('State Icon'), 'get_state_icon')
|
||||
history_state = fields.Function(
|
||||
fields.Selection(STATES, 'History state'),
|
||||
'get_history_state', searcher='search_history_state')
|
||||
history = fields.One2Many('stock.location.history', 'location',
|
||||
'History', readonly=True, loading='lazy')
|
||||
state_at = fields.Function(
|
||||
fields.Selection(STATES, 'State at'),
|
||||
'get_state_at', searcher='search_state_at')
|
||||
states = fields.One2Many('stock.location.state', 'location',
|
||||
'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
|
||||
def __setup__(cls):
|
||||
|
@ -66,7 +82,7 @@ class Location(metaclass=PoolMeta):
|
|||
if default is None:
|
||||
default = {}
|
||||
default = default.copy()
|
||||
default.setdefault('history', None)
|
||||
default.setdefault('states', None)
|
||||
return super(Location, cls).copy(records, default=default)
|
||||
|
||||
@classmethod
|
||||
|
@ -79,7 +95,7 @@ class Location(metaclass=PoolMeta):
|
|||
return None
|
||||
|
||||
@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}
|
||||
at_date = Transaction().context.get('state_at_date', None)
|
||||
|
@ -87,35 +103,38 @@ class Location(metaclass=PoolMeta):
|
|||
return values
|
||||
|
||||
for record in records:
|
||||
_history = sorted(record.history, key=lambda h: h.date)
|
||||
for _hist in _history:
|
||||
if _hist.date > at_date:
|
||||
for state in record.states:
|
||||
if state.date <= at_date:
|
||||
values[record.id] = state.state
|
||||
break
|
||||
values[record.id] = _hist.state
|
||||
return values
|
||||
|
||||
@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)
|
||||
if not at_date:
|
||||
return [('state', ) + tuple(clause[1:])]
|
||||
|
||||
location_history = cls.__table_history__()
|
||||
location_state = State.__table__()
|
||||
location_state2 = State.__table__()
|
||||
Operator = fields.SQL_OPERATORS[clause[1]]
|
||||
columns = [location_history.id.as_('location'),
|
||||
location_history.state.as_('state'),
|
||||
Max(Coalesce(location_history.write_date,
|
||||
location_history.create_date)).as_('date')]
|
||||
columns = [
|
||||
location_state.location,
|
||||
Max(location_state.id).as_('state_id'),
|
||||
Max(location_state.date).as_('date'),
|
||||
]
|
||||
|
||||
# Gets state of allowed max date
|
||||
query = location_history.select(
|
||||
query = location_state.select(
|
||||
*columns,
|
||||
where=(Coalesce(location_history.write_date,
|
||||
location_history.create_date) <= at_date),
|
||||
group_by=[location_history.id, location_history.state])
|
||||
query = query.select(
|
||||
query.location,
|
||||
where=(Operator(query.state, clause[2])))
|
||||
where=(location_state.date <= at_date),
|
||||
group_by=[location_state.location])
|
||||
query = query.join(location_state2, condition=(
|
||||
query.state_id == location_state2.id)
|
||||
).select(
|
||||
query.location,
|
||||
where=(Operator(location_state2.state, clause[2])))
|
||||
return [('id', 'in', query)]
|
||||
|
||||
@classmethod
|
||||
|
@ -162,55 +181,261 @@ class Location(metaclass=PoolMeta):
|
|||
('//page[@id="history"]', 'states', {
|
||||
'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):
|
||||
"""Stock location History"""
|
||||
__name__ = 'stock.location.history'
|
||||
# Gets last states
|
||||
query = location_state.select(
|
||||
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')
|
||||
date = fields.Function(fields.DateTime('Change Date'), 'get_date')
|
||||
location = fields.Many2One('stock.location', 'Location')
|
||||
user = fields.Many2One('res.user', 'User')
|
||||
state = fields.Selection(STATES, 'State')
|
||||
@classmethod
|
||||
def set_last_states(cls, records, name, value):
|
||||
with Transaction().set_context(create_history_state=False):
|
||||
cls.write(records, {
|
||||
'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
|
||||
def __setup__(cls):
|
||||
super(LocationHistory, cls).__setup__()
|
||||
cls._order.insert(0, ('date_', 'DESC'))
|
||||
super().__setup__()
|
||||
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
|
||||
def table_query(cls):
|
||||
_Location = Pool().get('stock.location')
|
||||
location_history = _Location.__table_history__()
|
||||
columns = [
|
||||
Min(Column(location_history, '__id')).as_('id'),
|
||||
location_history.id.as_('location'),
|
||||
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)
|
||||
def __register__(cls, module_name):
|
||||
TableHandler = backend.get('TableHandler')
|
||||
Location = Pool().get('stock.location')
|
||||
cursor = Transaction().connection.cursor()
|
||||
table = cls.__table__()
|
||||
location = Location.__table__()
|
||||
|
||||
return location_history.select(*columns, group_by=group_by)
|
||||
super().__register__(module_name)
|
||||
|
||||
def get_date(self, name):
|
||||
_date = self.date_
|
||||
if not isinstance(_date, datetime):
|
||||
_date = datetime.strptime(_date, '%Y-%m-%d %H:%M:%S.%f')
|
||||
return _date
|
||||
cursor.execute(*table.select(table.id, limit=1))
|
||||
if TableHandler.table_exist('stock_location__history') and \
|
||||
not cursor.fetchone():
|
||||
history = Table('stock_location__history')
|
||||
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):
|
||||
|
|
42
location.xml
42
location.xml
|
@ -14,26 +14,44 @@ this repository contains the full copyright notices and license terms. -->
|
|||
<field name="name">location_form</field>
|
||||
<field name="inherit" ref="stock.location_view_form"/>
|
||||
</record>
|
||||
<!-- Location history -->
|
||||
<record model="ir.ui.view" id="location_history_view_tree">
|
||||
<field name="model">stock.location.history</field>
|
||||
|
||||
<!-- Location state -->
|
||||
<record model="ir.ui.view" id="location_state_view_tree">
|
||||
<field name="model">stock.location.state</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">location_history_tree</field>
|
||||
<field name="name">location_state_tree</field>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_location_history">
|
||||
<field name="model" search="[('model', '=', 'stock.location.history')]"/>
|
||||
<record model="ir.model.access" id="access_location_state">
|
||||
<field name="model" search="[('model', '=', 'stock.location.state')]"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_location_history_stock">
|
||||
<field name="model" search="[('model', '=', 'stock.location.history')]"/>
|
||||
<field name="group" ref="stock.group_stock"/>
|
||||
<record model="ir.model.access" id="access_location_state_stock">
|
||||
<field name="model" search="[('model', '=', 'stock.location.state')]"/>
|
||||
<field name="group" ref="stock.group_stock_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<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>
|
||||
|
||||
<!-- Buttons -->
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# copyright notices and license terms.
|
||||
import unittest
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import trytond.tests.test_tryton
|
||||
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
|
||||
|
@ -17,14 +18,18 @@ class LocationStateHistoryTestCase(ModuleTestCase):
|
|||
def test_change_state(self):
|
||||
"""Change location state"""
|
||||
Location = Pool().get('stock.location')
|
||||
State = Pool().get('stock.location.state')
|
||||
transaction = Transaction()
|
||||
|
||||
production, = Location.create([
|
||||
{'code': 'PL',
|
||||
'name': 'Production location',
|
||||
'type': 'production'}])
|
||||
production, = Location.create([{
|
||||
'code': 'PL',
|
||||
'name': 'Production location',
|
||||
'type': 'production'
|
||||
}])
|
||||
self.assertEqual(production.state, 'on')
|
||||
transaction.commit()
|
||||
from_date = datetime.now().replace(microsecond=0
|
||||
) - relativedelta(seconds=5)
|
||||
time.sleep(2)
|
||||
|
||||
Location.off([production])
|
||||
|
@ -32,35 +37,70 @@ class LocationStateHistoryTestCase(ModuleTestCase):
|
|||
|
||||
transaction.commit()
|
||||
time.sleep(2)
|
||||
child, = Location.create([
|
||||
{'code': 'PL2',
|
||||
'name': 'Prod. location 2',
|
||||
'type': 'production',
|
||||
'parent': production.id,
|
||||
'state': 'off'}])
|
||||
child, = Location.create([{
|
||||
'code': 'PL2',
|
||||
'name': 'Prod. location 2',
|
||||
'type': 'production',
|
||||
'parent': production.id,
|
||||
'state': 'off'
|
||||
}])
|
||||
time.sleep(1)
|
||||
Location.on([production])
|
||||
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 = {}
|
||||
for line in production.history:
|
||||
for line in production.states:
|
||||
values.setdefault(line.state, 0)
|
||||
values[line.state] += 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)
|
||||
|
||||
with Transaction().set_context(state_at_date=at_date):
|
||||
other_loc, = Location.search([
|
||||
('code', '=', 'PL'),
|
||||
('history_state', '=', 'off')])
|
||||
('state_at', '=', 'off')])
|
||||
self.assertEqual(production.id, other_loc.id)
|
||||
self.assertEqual(production.get_history_state(
|
||||
self.assertEqual(production.get_state_at(
|
||||
[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():
|
||||
|
|
|
@ -7,6 +7,7 @@ depends:
|
|||
|
||||
extras_depend:
|
||||
stock_location_combined
|
||||
stock_unit_load
|
||||
|
||||
xml:
|
||||
location.xml
|
||||
|
|
|
@ -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)
|
|
@ -3,8 +3,8 @@
|
|||
this repository contains the full copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="/form/notebook" position="inside">
|
||||
<page id="history" string="History">
|
||||
<field name="history" colspan="4"/>
|
||||
<page name="states">
|
||||
<field name="last_states" colspan="4"/>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="/form" position="inside">
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- The COPYRIGHT file at the top level of this repository contains the full
|
||||
copyright notices and license terms. -->
|
||||
<tree>
|
||||
<tree editable="top">
|
||||
<field name="location"/>
|
||||
<field name="date" widget="date"/>
|
||||
<field name="date" widget="time" string="Change Time"/>
|
||||
<field name="user"/>
|
||||
<field name="date" widget="time" string="Time"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
|
@ -4,7 +4,7 @@ this repository contains the full copyright notices and license terms. -->
|
|||
<data>
|
||||
<xpath expr="/tree" position="inside">
|
||||
<field name="state" icon="state_icon"/>
|
||||
<button string="Start" name="on"/>
|
||||
<button string="Stop" name="off"/>
|
||||
<button name="on"/>
|
||||
<button name="off"/>
|
||||
</xpath>
|
||||
</data>
|
||||
|
|
Loading…
Reference in New Issue