first commit
This commit is contained in:
851
modules/quality/quality.py
Normal file
851
modules/quality/quality.py
Normal file
@@ -0,0 +1,851 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
import datetime as dt
|
||||
from functools import wraps
|
||||
from random import Random
|
||||
|
||||
from sql.functions import CharLength
|
||||
|
||||
from trytond.i18n import gettext, lazy_gettext
|
||||
from trytond.model import (
|
||||
ChatMixin, DeactivableMixin, DictSchemaMixin, MatchMixin, ModelSingleton,
|
||||
ModelSQL, ModelStorage, ModelView, Unique, Workflow, dualmethod, fields)
|
||||
from trytond.model.exceptions import AccessError, ButtonActionException
|
||||
from trytond.modules.company.model import (
|
||||
CompanyMultiValueMixin, CompanyValueMixin, employee_field, reset_employee,
|
||||
set_employee)
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval, Id, If
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
||||
|
||||
from .exceptions import InspectionError, InspectionValidationError
|
||||
|
||||
|
||||
class Configuration(
|
||||
ModelSingleton, CompanyMultiValueMixin, ModelSQL, ModelView):
|
||||
__name__ = 'quality.configuration'
|
||||
|
||||
inspection_sequence = fields.MultiValue(fields.Many2One(
|
||||
'ir.sequence', "Inspection Sequence", required=True,
|
||||
domain=[
|
||||
('company', 'in', [
|
||||
Eval('context', {}).get('company', -1),
|
||||
None]),
|
||||
('sequence_type', '=',
|
||||
Id('quality', 'sequence_type_quality_inspection')),
|
||||
]))
|
||||
alert_sequence = fields.MultiValue(fields.Many2One(
|
||||
'ir.sequence', "Alert Sequence", required=True,
|
||||
domain=[
|
||||
('company', 'in', [
|
||||
Eval('context', {}).get('company', -1),
|
||||
None]),
|
||||
('sequence_type', '=',
|
||||
Id('quality', 'sequence_type_quality_alert')),
|
||||
]))
|
||||
|
||||
@classmethod
|
||||
def multivalue_model(cls, field):
|
||||
pool = Pool()
|
||||
if field in {'inspection_sequence', 'alert_sequence'}:
|
||||
return pool.get('quality.configuration.sequence')
|
||||
return super().multivalue_model(field)
|
||||
|
||||
@classmethod
|
||||
def default_inspection_sequence(cls, **pattern):
|
||||
return (
|
||||
cls.multivalue_model('inspection_sequence')
|
||||
.default_inspection_sequence())
|
||||
|
||||
@classmethod
|
||||
def default_alert_sequence(cls, **pattern):
|
||||
return (
|
||||
cls.multivalue_model('alert_sequence')
|
||||
.default_alert_sequence())
|
||||
|
||||
|
||||
class ConfigurationSequence(CompanyValueMixin, ModelSQL):
|
||||
__name__ = 'quality.configuration.sequence'
|
||||
inspection_sequence = fields.Many2One(
|
||||
'ir.sequence', "Inspection Sequence", required=True,
|
||||
domain=[
|
||||
('company', 'in', [Eval('company', -1), None]),
|
||||
('sequence_type', '=',
|
||||
Id('quality', 'sequence_type_quality_inspection')),
|
||||
])
|
||||
alert_sequence = fields.Many2One(
|
||||
'ir.sequence', "Alert Sequence", required=True,
|
||||
domain=[
|
||||
('company', 'in', [Eval('company', -1), None]),
|
||||
('sequence_type', '=',
|
||||
Id('quality', 'sequence_type_quality_alert')),
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def default_inspection_sequence(cls):
|
||||
pool = Pool()
|
||||
ModelData = pool.get('ir.model.data')
|
||||
try:
|
||||
return ModelData.get_id('quality', 'sequence_quality_inspection')
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def default_alert_sequence(cls):
|
||||
pool = Pool()
|
||||
ModelData = pool.get('ir.model.data')
|
||||
try:
|
||||
return ModelData.get_id('quality', 'sequence_quality_alert')
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
class ControlledMixin(ModelStorage):
|
||||
__slots__ = ()
|
||||
|
||||
quality_inspections = fields.One2Many(
|
||||
'quality.inspection', 'origin',
|
||||
lazy_gettext('quality.msg_quality_inspections'), readonly=True)
|
||||
|
||||
@classmethod
|
||||
def copy(cls, records, default=None):
|
||||
default = default.copy() if default is not None else {}
|
||||
default.setdefault('quality_inspections')
|
||||
return super().copy(records, default=default)
|
||||
|
||||
def quality_control_pattern(self, operation):
|
||||
return {}
|
||||
|
||||
@dualmethod
|
||||
def quality_control_needed(cls, records, operation):
|
||||
pool = Pool()
|
||||
Control = pool.get('quality.control')
|
||||
return bool(Control.get_inspections(records, operation))
|
||||
|
||||
def quality_controlled_for(self, operation):
|
||||
operation = f'{self.__name__}:{operation}'
|
||||
return any(
|
||||
i for i in self.quality_inspections
|
||||
if operation in i.control.operations)
|
||||
|
||||
def quality_inspections_pending(self):
|
||||
return [i for i in self.quality_inspections if i.state == 'pending']
|
||||
|
||||
def quality_inspections_failed(self):
|
||||
return [
|
||||
i for i in self.quality_inspections
|
||||
if i.state == 'failed'
|
||||
and (any(a.state in {'open', 'processing'} for a in i.alerts)
|
||||
or not i.alerts)]
|
||||
|
||||
@staticmethod
|
||||
def control(operation, wizard):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(cls, records):
|
||||
if (cls.quality_control_needed(records, operation)
|
||||
or any(
|
||||
r.quality_inspections_pending() for r in records)):
|
||||
raise ButtonActionException(wizard)
|
||||
if any(r.quality_inspections_failed() for r in records):
|
||||
raise InspectionError(gettext(
|
||||
'quality.msg_quality_inspection_failed'))
|
||||
return func(cls, records)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class Control(DeactivableMixin, MatchMixin, ModelSQL, ModelView):
|
||||
__name__ = 'quality.control'
|
||||
|
||||
name = fields.Char("Name", required=True, translate=True)
|
||||
|
||||
operations = fields.MultiSelection([
|
||||
('stock.shipment.in:receive', "Supplier Shipment Received"),
|
||||
('stock.shipment.in:do', "Supplier Shipment Done"),
|
||||
('stock.shipment.out:pick', "Customer Shipment Picked"),
|
||||
('stock.shipment.out:pack', "Customer Shipment Packed"),
|
||||
('stock.shipment.out.return:receive',
|
||||
"Customer Shipment Return Received"),
|
||||
('stock.shipment.out.return:do',
|
||||
"Customer Shipment Return Done"),
|
||||
('stock.shipment.internal:ship', "Internal Shipment Shipped"),
|
||||
('stock.shipment.internal:do', "Internal Shipment Done"),
|
||||
('production:run', "Production Run"),
|
||||
('production:do', "Production Done"),
|
||||
], "Operations",
|
||||
help="The operations for which the control is performed.")
|
||||
frequency = fields.Float(
|
||||
"Frequency", digits=(1, 4), required=True,
|
||||
domain=[
|
||||
('frequency', '>=', 0),
|
||||
('frequency', '<=', 1),
|
||||
],
|
||||
help="How often the control must be done.")
|
||||
company = fields.Many2One(
|
||||
'company.company', "Company", ondelete='RESTRICT',
|
||||
help="The company to which the control applies.")
|
||||
product_category = fields.Many2One(
|
||||
'product.category', "Product Category", ondelete='RESTRICT',
|
||||
help="The product category to which the control applies.")
|
||||
product = fields.Many2One(
|
||||
'product.product', 'Product', ondelete='RESTRICT',
|
||||
context={
|
||||
'company': Eval('company', Eval('context', {}).get('company')),
|
||||
},
|
||||
help="The product to which the control applies.")
|
||||
|
||||
points = fields.One2Many(
|
||||
'quality.control.point', 'control', "Points", required=True)
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module):
|
||||
table = cls.__table__()
|
||||
transaction = Transaction()
|
||||
database = transaction.database
|
||||
cursor = transaction.connection.cursor()
|
||||
update = transaction.connection.cursor()
|
||||
|
||||
super().__register__(module)
|
||||
|
||||
# Migration from 7.0: rename done button to do
|
||||
for old, new in [
|
||||
('stock.shipment.in:done', 'stock.shipment.in:do'),
|
||||
('stock.shipment.out.return:done',
|
||||
'stock.shipment.out.return:do'),
|
||||
('stock.shipment.internal:done',
|
||||
'stock.shipment.internal:do'),
|
||||
('production:done', 'production:do'),
|
||||
]:
|
||||
try:
|
||||
where = database.json_key_exists(table.operations, old)
|
||||
except NotImplementedError:
|
||||
where = table.operations.like(f'%{old}%')
|
||||
cursor.execute(*table.select(
|
||||
table.id, table.operations, where=where))
|
||||
for id_, operations in cursor:
|
||||
if isinstance(operations, (list, tuple)):
|
||||
value = cls.operations.sql_format(
|
||||
[x.replace(old, new) for x in operations])
|
||||
else:
|
||||
value = operations.replace(old, new)
|
||||
update.execute(*table.update(
|
||||
[table.operations],
|
||||
[value],
|
||||
where=table.id == id_))
|
||||
|
||||
@classmethod
|
||||
def default_frequency(cls):
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def get_inspections(cls, records, operation):
|
||||
pool = Pool()
|
||||
Inspection = pool.get('quality.inspection')
|
||||
|
||||
records = [
|
||||
r for r in records if not r.quality_controlled_for(operation)]
|
||||
inspections = []
|
||||
if records:
|
||||
name = records[0].__name__
|
||||
assert len({r.__name__ for r in records}) == 1
|
||||
|
||||
controls = cls.search([
|
||||
('operations', 'in', f'{name}:{operation}'),
|
||||
])
|
||||
for control in controls:
|
||||
for record in records:
|
||||
pattern = record.quality_control_pattern(operation)
|
||||
if control.match(pattern) and control.choose(record):
|
||||
inspections.append(
|
||||
Inspection.get_from_control(control, record))
|
||||
return inspections
|
||||
|
||||
def match(self, pattern, match_none=False):
|
||||
pool = Pool()
|
||||
Product = pool.get('product.product')
|
||||
pattern = pattern.copy()
|
||||
|
||||
def parents(categories):
|
||||
for category in categories:
|
||||
while category:
|
||||
yield category
|
||||
category = category.parent
|
||||
|
||||
products = set(pattern.pop('products', []))
|
||||
if pattern.get('product'):
|
||||
products.add(pattern.pop('product'))
|
||||
if products:
|
||||
products = Product.browse(products)
|
||||
if self.product and self.product not in products:
|
||||
return False
|
||||
if self.product_category:
|
||||
categories = {
|
||||
c for p in products for c in parents(p.categories_all)}
|
||||
if self.product_category not in categories:
|
||||
return False
|
||||
return super().match(pattern, match_none=match_none)
|
||||
|
||||
def choose(self, record):
|
||||
random = Random(record.id)
|
||||
return random.random() <= self.frequency
|
||||
|
||||
|
||||
class ControlPoint(DictSchemaMixin, ModelSQL, ModelView):
|
||||
__name__ = 'quality.control.point'
|
||||
|
||||
control = fields.Many2One(
|
||||
'quality.control', "Control", required=True, ondelete='CASCADE')
|
||||
tolerance_lower = fields.Float(
|
||||
"Tolerance Lower",
|
||||
states={
|
||||
'invisible': ~Eval('type_').in_(['integer', 'float', 'numeric']),
|
||||
})
|
||||
tolerance_upper = fields.Float(
|
||||
"Tolerance Upper",
|
||||
states={
|
||||
'invisible': ~Eval('type_').in_(['integer', 'float', 'numeric']),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_constraints = [
|
||||
('control_point_unique', Unique(t, t.control, t.name),
|
||||
'quality.msg_control_point_unique'),
|
||||
]
|
||||
cls.__access__.add('control')
|
||||
cls.type_.selection = [
|
||||
('boolean', lazy_gettext('ir.msg_dict_schema_boolean')),
|
||||
('integer', lazy_gettext('ir.msg_dict_schema_integer')),
|
||||
('float', lazy_gettext('ir.msg_dict_schema_float')),
|
||||
('numeric', lazy_gettext('ir.msg_dict_schema_numeric')),
|
||||
]
|
||||
|
||||
def check(self, value):
|
||||
if self.type_ == 'boolean':
|
||||
return value
|
||||
elif self.type_ in {'integer', 'float', 'numeric'}:
|
||||
result = True
|
||||
if value is None:
|
||||
result = False
|
||||
elif (self.tolerance_lower is not None
|
||||
and self.tolerance_lower > value):
|
||||
result = False
|
||||
elif (self.tolerance_upper is not None
|
||||
and self.tolerance_upper < value):
|
||||
result = False
|
||||
return result
|
||||
|
||||
|
||||
class Inspection(Workflow, ModelSQL, ModelView, ChatMixin):
|
||||
__name__ = 'quality.inspection'
|
||||
_rec_name = 'number'
|
||||
|
||||
_states = {
|
||||
'readonly': Eval('state') != 'pending',
|
||||
}
|
||||
|
||||
number = fields.Char("Number", required=True, readonly=True)
|
||||
company = fields.Many2One(
|
||||
'company.company', "Company", required=True, states=_states)
|
||||
origin = fields.Reference(
|
||||
"Origin", 'get_origins', states=_states,
|
||||
domain={
|
||||
'stock.shipment.in': [
|
||||
('company', '=', Eval('company', -1)),
|
||||
],
|
||||
'stock.shipment.out': [
|
||||
('company', '=', Eval('company', -1)),
|
||||
],
|
||||
'stock.shipment.out.return': [
|
||||
('company', '=', Eval('company', -1)),
|
||||
],
|
||||
'stock.shipment.internal': [
|
||||
('company', '=', Eval('company', -1)),
|
||||
],
|
||||
'production': [
|
||||
('company', '=', Eval('company', -1)),
|
||||
],
|
||||
})
|
||||
control = fields.Many2One(
|
||||
'quality.control', "Control", required=True, states=_states,
|
||||
domain=[
|
||||
('company', 'in', [Eval('company', -1), None]),
|
||||
])
|
||||
|
||||
points = fields.Dict(
|
||||
'quality.control.point', "Points",
|
||||
domain=[
|
||||
('control', '=', Eval('control', -1)),
|
||||
],
|
||||
states=_states)
|
||||
|
||||
alerts = fields.One2Many(
|
||||
'quality.alert', 'origin', "Alerts",
|
||||
states={
|
||||
'invisible': (
|
||||
(Eval('state') != 'failed')
|
||||
| ~Eval('alerts')),
|
||||
})
|
||||
|
||||
processed_by = employee_field("Processed by", states=['passed', 'failed'])
|
||||
processed_at = fields.DateTime("Processed at", states=_states)
|
||||
passed_by = employee_field("Passed by", states=['passed'])
|
||||
failed_by = employee_field("Failed by", states=['failed'])
|
||||
|
||||
state = fields.Selection([
|
||||
('pending', "Pending"),
|
||||
('passed', "Passed"),
|
||||
('failed', "Failed"),
|
||||
], "State", required=True, readonly=True, sort=False)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
cls.number.search_unaccented = False
|
||||
super().__setup__()
|
||||
cls._transitions |= {
|
||||
('pending', 'passed'),
|
||||
('pending', 'failed'),
|
||||
('passed', 'failed'),
|
||||
('passed', 'pending'),
|
||||
('failed', 'passed'),
|
||||
('failed', 'pending'),
|
||||
}
|
||||
cls._buttons.update({
|
||||
'pending': {
|
||||
'invisible': ~Eval('state').in_(['passed', 'failed']),
|
||||
'icon': 'tryton-back',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'process': {
|
||||
'invisible': Eval('state') != 'pending',
|
||||
'icon': 'tryton-forward',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'pass_': {
|
||||
'invisible': Eval('state') != 'failed',
|
||||
'icon': 'tryton-ok',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'fail': {
|
||||
'invisible': Eval('state') != 'passed',
|
||||
'icon': 'tryton-cancel',
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def order_number(cls, tables):
|
||||
table, _ = tables[None]
|
||||
return [CharLength(table.number), table.number]
|
||||
|
||||
@classmethod
|
||||
def default_company(cls):
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@classmethod
|
||||
def default_state(cls):
|
||||
return 'pending'
|
||||
|
||||
@classmethod
|
||||
def _get_origins(cls):
|
||||
return [
|
||||
'stock.shipment.in',
|
||||
'stock.shipment.out',
|
||||
'stock.shipment.out.return',
|
||||
'stock.shipment.internal',
|
||||
'production',
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_origins(cls):
|
||||
pool = Pool()
|
||||
IrModel = pool.get('ir.model')
|
||||
models = cls._get_origins()
|
||||
models = IrModel.search([
|
||||
('name', 'in', models),
|
||||
])
|
||||
return [(None, '')] + [(m.name, m.string) for m in models]
|
||||
|
||||
@fields.depends('control', 'points')
|
||||
def on_change_control(self):
|
||||
if self.control:
|
||||
points = dict(self.points) if self.points is not None else {}
|
||||
points.update(
|
||||
(p.name, None) for p in self.control.points)
|
||||
self.points = points
|
||||
|
||||
def validate_points(self):
|
||||
points = {p.name: p.string for p in self.control.points}
|
||||
missing = points.keys() - (self.points or {}).keys()
|
||||
if missing:
|
||||
raise InspectionValidationError(
|
||||
gettext('quality.msg_quality_inspection_missing_point',
|
||||
inspection=self.rec_name,
|
||||
points=', '.join(points[m] for m in missing),
|
||||
))
|
||||
|
||||
def check(self):
|
||||
for point in self.control.points:
|
||||
if not point.check(self.points.get(point.name)):
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def view_attributes(cls):
|
||||
attributes = super().view_attributes() + [
|
||||
('/tree', 'visual', If(Eval('state') == 'failed', 'danger', '')),
|
||||
]
|
||||
if Transaction().context.get('inspect'):
|
||||
attributes.extend([
|
||||
('//field[@name="control"]', 'readonly', 1),
|
||||
('//*[@name="origin"]', 'invisible', 1),
|
||||
('//page[@id="other"]', 'invisible', 1),
|
||||
('//*[@name="state"]', 'invisible', 1),
|
||||
('//group[@id="buttons"]', 'states', {'invisible': True}),
|
||||
])
|
||||
|
||||
return attributes
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('pending')
|
||||
@reset_employee('processed_by', 'passed_by', 'failed_by')
|
||||
def pending(cls, inspections):
|
||||
cls.write(inspections, {
|
||||
'processed_at': None,
|
||||
})
|
||||
|
||||
@dualmethod
|
||||
@ModelView.button
|
||||
@set_employee('processed_by')
|
||||
def process(cls, inspections):
|
||||
for inspection in inspections:
|
||||
inspection.validate_points()
|
||||
cls.write([i for i in inspections if not i.processed_at], {
|
||||
'processed_at': dt.datetime.now(),
|
||||
})
|
||||
to_pass, to_fail = [], []
|
||||
for inspection in inspections:
|
||||
if inspection.check():
|
||||
to_pass.append(inspection)
|
||||
else:
|
||||
to_fail.append(inspection)
|
||||
cls.pass_(to_pass)
|
||||
cls.fail(to_fail)
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('passed')
|
||||
@reset_employee('failed_by')
|
||||
@set_employee('passed_by')
|
||||
def pass_(cls, inspections):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('failed')
|
||||
@reset_employee('passed_by')
|
||||
@set_employee('failed_by')
|
||||
def fail(cls, inspections):
|
||||
pool = Pool()
|
||||
Alert = pool.get('quality.alert')
|
||||
alerts = []
|
||||
for inspection in inspections:
|
||||
if not inspection.alerts:
|
||||
alerts.append(inspection.get_alert())
|
||||
Alert.save(alerts)
|
||||
|
||||
def get_alert(self):
|
||||
pool = Pool()
|
||||
Alert = pool.get('quality.alert')
|
||||
return Alert(
|
||||
company=self.company,
|
||||
origin=self)
|
||||
|
||||
@classmethod
|
||||
def preprocess_values(cls, mode, values):
|
||||
pool = Pool()
|
||||
Configuration = pool.get('quality.configuration')
|
||||
values = super().preprocess_values(mode, values)
|
||||
if mode == 'create' and not values.get('number'):
|
||||
company_id = values.get('company', cls.default_company())
|
||||
if company_id is not None:
|
||||
configuration = Configuration(1)
|
||||
if sequence := configuration.get_multivalue(
|
||||
'inspection_sequence', company=company_id):
|
||||
values['number'] = sequence.get()
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def check_modification(
|
||||
cls, mode, inspections, values=None, external=False):
|
||||
super().check_modification(
|
||||
mode, inspections, values=values, external=external)
|
||||
if mode == 'delete':
|
||||
for inspection in inspections:
|
||||
if inspection.state != 'pending':
|
||||
raise AccessError(gettext(
|
||||
'quality.msg_inspection_delete_non_pending',
|
||||
inspection=inspection.rec_name))
|
||||
|
||||
@classmethod
|
||||
def copy(cls, inspections, default=None):
|
||||
default = default.copy() if default is not None else {}
|
||||
default.setdefault('number')
|
||||
default.setdefault('processed_by')
|
||||
default.setdefault('processed_at')
|
||||
default.setdefault('passed_by')
|
||||
default.setdefault('failed_by')
|
||||
return super().copy(inspections, default=default)
|
||||
|
||||
@classmethod
|
||||
def get_from_control(cls, control, origin=None):
|
||||
return cls(control=control, origin=origin)
|
||||
|
||||
|
||||
class Alert(Workflow, ModelSQL, ModelView, ChatMixin):
|
||||
__name__ = 'quality.alert'
|
||||
_rec_name = 'number'
|
||||
|
||||
_states = {
|
||||
'readonly': ~Eval('state').in_(['open', 'processing']),
|
||||
}
|
||||
|
||||
number = fields.Char("Number", required=True, readonly=True)
|
||||
company = fields.Many2One(
|
||||
'company.company', "Company", required=True, states=_states)
|
||||
title = fields.Char("Title")
|
||||
origin = fields.Reference(
|
||||
"Origin", 'get_origins', required=True,
|
||||
domain={
|
||||
'quality.inspection': [
|
||||
('company', '=', Eval('company', -1)),
|
||||
],
|
||||
})
|
||||
description = fields.Text("Description")
|
||||
|
||||
processed_by = employee_field(
|
||||
"Processed by", states=['processing', 'resolved', 'deferred'])
|
||||
deferred_by = employee_field(
|
||||
"Deferred by", states=['deferred', 'resolved'])
|
||||
resolved_by = employee_field("Resolved by", states=['resolved'])
|
||||
|
||||
state = fields.Selection([
|
||||
('open', "Open"),
|
||||
('processing', "Processing"),
|
||||
('deferred', "Deferred"),
|
||||
('resolved', "Resolved"),
|
||||
], "State", required=True, readonly=True, sort=False)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
cls.number.search_unaccented = False
|
||||
super().__setup__()
|
||||
cls._transitions |= {
|
||||
('open', 'processing'),
|
||||
('processing', 'resolved'),
|
||||
('processing', 'deferred'),
|
||||
('deferred', 'processing'),
|
||||
('resolved', 'open'),
|
||||
}
|
||||
cls._buttons.update({
|
||||
'open': {
|
||||
'invisible': Eval('state') != 'resolved',
|
||||
'icon': 'tryton-back',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'process': {
|
||||
'invisible': ~Eval('state').in_(['open', 'deferred']),
|
||||
'icon': 'tryton-forward',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'defer': {
|
||||
'invisible': Eval('state') != 'processing',
|
||||
'icon': 'tryton-archive',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'resolve': {
|
||||
'invisible': Eval('state') != 'processing',
|
||||
'icon': 'tryton-ok',
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def default_company(cls):
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@classmethod
|
||||
def default_state(cls):
|
||||
return 'open'
|
||||
|
||||
@classmethod
|
||||
def _get_origins(cls):
|
||||
return ['quality.inspection']
|
||||
|
||||
@classmethod
|
||||
def get_origins(cls):
|
||||
pool = Pool()
|
||||
IrModel = pool.get('ir.model')
|
||||
models = cls._get_origins()
|
||||
models = IrModel.search([
|
||||
('name', 'in', models),
|
||||
])
|
||||
return [(None, '')] + [(m.name, m.string) for m in models]
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('open')
|
||||
@reset_employee('processed_by', 'deferred_by', 'resolved_by')
|
||||
def open(cls, alerts):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('processing')
|
||||
@set_employee('processed_by')
|
||||
def process(cls, alerts):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('deferred')
|
||||
@reset_employee('processed_by')
|
||||
@set_employee('deferred_by')
|
||||
def defer(cls, alerts):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('resolved')
|
||||
@set_employee('resolved_by')
|
||||
def resolve(cls, alerts):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def preprocess_values(cls, mode, values):
|
||||
pool = Pool()
|
||||
Configuration = pool.get('quality.configuration')
|
||||
values = super().preprocess_values(mode, values)
|
||||
if mode == 'create' and not values.get('number'):
|
||||
company_id = values.get('company', cls.default_company())
|
||||
if company_id is not None:
|
||||
configuration = Configuration(1)
|
||||
if sequence := configuration.get_multivalue(
|
||||
'alert_sequence', company=company_id):
|
||||
values['number'] = sequence.get()
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def copy(cls, inspections, default=None):
|
||||
default = default.copy() if default is not None else {}
|
||||
default.setdefault('number')
|
||||
default.setdefault('processed_by')
|
||||
default.setdefault('deferred_by')
|
||||
default.setdefault('resolved_by')
|
||||
return super().copy(inspections, default=default)
|
||||
|
||||
|
||||
class InspectStateView(StateView):
|
||||
def get_view(self, wizard, state_name):
|
||||
with Transaction().set_context(inspect=True):
|
||||
return super().get_view(wizard, state_name)
|
||||
|
||||
def get_defaults(self, wizard, state_name, fields):
|
||||
return {}
|
||||
|
||||
|
||||
class Inspect(Wizard):
|
||||
__name__ = 'quality.inspect'
|
||||
|
||||
start = StateTransition()
|
||||
store = StateView('quality.inspect.store', None, [])
|
||||
next_ = StateTransition()
|
||||
inspection = InspectStateView(
|
||||
'quality.inspection', 'quality.quality_inspection_view_form', [
|
||||
Button("Cancel", 'end', 'tryton-cancel'),
|
||||
Button("Skip", 'next_', 'tryton-forward', validate=False),
|
||||
Button("Save", 'save', 'tryton-ok', default=True),
|
||||
])
|
||||
save = StateTransition()
|
||||
|
||||
def transition_start(self):
|
||||
pool = Pool()
|
||||
Control = pool.get('quality.control')
|
||||
Inspection = pool.get('quality.inspection')
|
||||
Store = pool.get('quality.inspect.store')
|
||||
|
||||
context = Transaction().context
|
||||
operation = self.operation(context.get('action_id'))
|
||||
|
||||
inspections = Control.get_inspections(self.records, operation)
|
||||
Inspection.save(inspections)
|
||||
|
||||
inspections = []
|
||||
for record in self.records:
|
||||
inspections.extend(record.quality_inspections_pending())
|
||||
self.store = Store(inspections=inspections)
|
||||
return 'inspection' if self.store.inspections else 'end'
|
||||
|
||||
@property
|
||||
def _operations(self):
|
||||
return {
|
||||
'quality.wizard_stock_shipment_in_inspect_receive': 'receive',
|
||||
'quality.wizard_stock_shipment_in_inspect_done': 'done',
|
||||
'quality.wizard_stock_shipment_out_inspect_pick': 'pick',
|
||||
'quality.wizard_stock_shipment_out_inspect_pack': 'pack',
|
||||
'quality.wizard_stock_shipment_out_return_inspect_receive':
|
||||
'receive',
|
||||
'quality.wizard_stock_shipment_out_return_inspect_done': 'done',
|
||||
'quality.wizard_stock_shipment_internal_inspect_ship': 'ship',
|
||||
'quality.wizard_stock_shipment_internal_inspect_done': 'done',
|
||||
'quality.wizard_production_inspect_run': 'run',
|
||||
'quality.wizard_production_inspect_done': 'done',
|
||||
}
|
||||
|
||||
def operation(self, action_id):
|
||||
pool = Pool()
|
||||
ModelData = pool.get('ir.model.data')
|
||||
for xml_id, operation in self._operations.items():
|
||||
try:
|
||||
if action_id == ModelData.get_id(xml_id):
|
||||
return operation
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
def transition_next_(self):
|
||||
self.store.inspections = self.store.inspections[1:]
|
||||
return 'inspection' if self.store.inspections else 'end'
|
||||
|
||||
def value_inspection(self, fields):
|
||||
inspection = self.store.inspections[0]
|
||||
values = {}
|
||||
for fieldname in fields:
|
||||
values[fieldname] = getattr(inspection, fieldname)
|
||||
|
||||
if 'points' in fields:
|
||||
# Convert ImmutableDict to dict
|
||||
values['points'] = dict(values['points'] or {})
|
||||
for point in inspection.control.points:
|
||||
values['points'].setdefault(point.name)
|
||||
return values
|
||||
|
||||
def transition_save(self):
|
||||
pool = Pool()
|
||||
Inspection = pool.get('quality.inspection')
|
||||
inspection = self.store.inspections[0]
|
||||
Inspection.write([inspection], self.inspection._save_values())
|
||||
inspection.process()
|
||||
return 'next_'
|
||||
|
||||
|
||||
class InspectStore(ModelView):
|
||||
__name__ = 'quality.inspect.store'
|
||||
|
||||
inspections = fields.Many2Many(
|
||||
'quality.inspection', None, None, "Inspections")
|
||||
Reference in New Issue
Block a user