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
|
# 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'])
|
||||||
|
|
74
locale/es.po
74
locale/es.po
|
@ -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"
|
381
location.py
381
location.py
|
@ -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):
|
||||||
|
|
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="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 -->
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -7,6 +7,7 @@ depends:
|
||||||
|
|
||||||
extras_depend:
|
extras_depend:
|
||||||
stock_location_combined
|
stock_location_combined
|
||||||
|
stock_unit_load
|
||||||
|
|
||||||
xml:
|
xml:
|
||||||
location.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. -->
|
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">
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue