质量模块和库存扫码
This commit is contained in:
8
quality_control/models/__init__.py
Normal file
8
quality_control/models/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import quality
|
||||
from . import stock_move
|
||||
from . import stock_move_line
|
||||
from . import stock_picking
|
||||
from . import stock_lot
|
||||
467
quality_control/models/quality.py
Normal file
467
quality_control/models/quality.py
Normal file
@@ -0,0 +1,467 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from math import sqrt
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import datetime
|
||||
|
||||
import random
|
||||
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_round
|
||||
from odoo.osv.expression import OR
|
||||
|
||||
|
||||
class QualityPoint(models.Model):
|
||||
_inherit = "quality.point"
|
||||
|
||||
failure_message = fields.Html('Failure Message')
|
||||
measure_on = fields.Selection([
|
||||
('operation', 'Operation'),
|
||||
('product', 'Product'),
|
||||
('move_line', 'Quantity')], string="Control per", default='product', required=True,
|
||||
help="""Operation = One quality check is requested at the operation level.
|
||||
Product = A quality check is requested per product.
|
||||
Quantity = A quality check is requested for each new product quantity registered, with partial quantity checks also possible.""")
|
||||
measure_frequency_type = fields.Selection([
|
||||
('all', 'All'),
|
||||
('random', 'Randomly'),
|
||||
('periodical', 'Periodically')], string="Control Frequency",
|
||||
default='all', required=True)
|
||||
measure_frequency_value = fields.Float('Percentage') # TDE RENAME ?
|
||||
measure_frequency_unit_value = fields.Integer('Frequency Unit Value') # TDE RENAME ?
|
||||
measure_frequency_unit = fields.Selection([
|
||||
('day', 'Days'),
|
||||
('week', 'Weeks'),
|
||||
('month', 'Months')], default="day") # TDE RENAME ?
|
||||
is_lot_tested_fractionally = fields.Boolean(string="Lot Tested Fractionally", help="Determines if only a fraction of the lot should be tested")
|
||||
testing_percentage_within_lot = fields.Float(help="Defines the percentage within a lot that should be tested")
|
||||
norm = fields.Float('Norm', digits='Quality Tests') # TDE RENAME ?
|
||||
tolerance_min = fields.Float('Min Tolerance', digits='Quality Tests')
|
||||
tolerance_max = fields.Float('Max Tolerance', digits='Quality Tests')
|
||||
norm_unit = fields.Char('Norm Unit', default=lambda self: 'mm') # TDE RENAME ?
|
||||
average = fields.Float(compute="_compute_standard_deviation_and_average")
|
||||
standard_deviation = fields.Float(compute="_compute_standard_deviation_and_average")
|
||||
|
||||
def _compute_standard_deviation_and_average(self):
|
||||
# The variance and mean are computed by the Welford’s method and used the Bessel's
|
||||
# correction because are working on a sample.
|
||||
for point in self:
|
||||
if point.test_type != 'measure':
|
||||
point.average = 0
|
||||
point.standard_deviation = 0
|
||||
continue
|
||||
mean = 0.0
|
||||
s = 0.0
|
||||
n = 0
|
||||
for check in point.check_ids.filtered(lambda x: x.quality_state != 'none'):
|
||||
n += 1
|
||||
delta = check.measure - mean
|
||||
mean += delta / n
|
||||
delta2 = check.measure - mean
|
||||
s += delta * delta2
|
||||
|
||||
if n > 1:
|
||||
point.average = mean
|
||||
point.standard_deviation = sqrt( s / ( n - 1))
|
||||
elif n == 1:
|
||||
point.average = mean
|
||||
point.standard_deviation = 0.0
|
||||
else:
|
||||
point.average = 0.0
|
||||
point.standard_deviation = 0.0
|
||||
|
||||
@api.onchange('norm')
|
||||
def onchange_norm(self):
|
||||
if self.tolerance_max == 0.0:
|
||||
self.tolerance_max = self.norm
|
||||
|
||||
def check_execute_now(self):
|
||||
self.ensure_one()
|
||||
if self.measure_frequency_type == 'all':
|
||||
return True
|
||||
elif self.measure_frequency_type == 'random':
|
||||
return (random.random() < self.measure_frequency_value / 100.0)
|
||||
elif self.measure_frequency_type == 'periodical':
|
||||
delta = False
|
||||
if self.measure_frequency_unit == 'day':
|
||||
delta = relativedelta(days=self.measure_frequency_unit_value)
|
||||
elif self.measure_frequency_unit == 'week':
|
||||
delta = relativedelta(weeks=self.measure_frequency_unit_value)
|
||||
elif self.measure_frequency_unit == 'month':
|
||||
delta = relativedelta(months=self.measure_frequency_unit_value)
|
||||
date_previous = datetime.today() - delta
|
||||
checks = self.env['quality.check'].search([
|
||||
('point_id', '=', self.id),
|
||||
('create_date', '>=', date_previous.strftime(DEFAULT_SERVER_DATETIME_FORMAT))], limit=1)
|
||||
return not(bool(checks))
|
||||
return super(QualityPoint, self).check_execute_now()
|
||||
|
||||
def _get_type_default_domain(self):
|
||||
domain = super(QualityPoint, self)._get_type_default_domain()
|
||||
domain.append(('technical_name', '=', 'passfail'))
|
||||
return domain
|
||||
|
||||
def action_see_quality_checks(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_check_action_main")
|
||||
action['domain'] = [('point_id', '=', self.id)]
|
||||
action['context'] = {
|
||||
'default_company_id': self.company_id.id,
|
||||
'default_point_id': self.id
|
||||
}
|
||||
return action
|
||||
|
||||
def action_see_spc_control(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_check_action_spc")
|
||||
if self.test_type == 'measure':
|
||||
action['context'] = {'group_by': ['name', 'point_id'], 'graph_measure': ['measure'], 'graph_mode': 'line'}
|
||||
action['domain'] = [('point_id', '=', self.id), ('quality_state', '!=', 'none')]
|
||||
return action
|
||||
|
||||
|
||||
class QualityCheck(models.Model):
|
||||
_inherit = "quality.check"
|
||||
|
||||
failure_message = fields.Html(related='point_id.failure_message', readonly=True)
|
||||
measure = fields.Float('Measure', default=0.0, digits='Quality Tests', tracking=True)
|
||||
measure_success = fields.Selection([
|
||||
('none', 'No measure'),
|
||||
('pass', 'Pass'),
|
||||
('fail', 'Fail')], string="Measure Success", compute="_compute_measure_success",
|
||||
readonly=True, store=True)
|
||||
tolerance_min = fields.Float('Min Tolerance', related='point_id.tolerance_min', readonly=True)
|
||||
tolerance_max = fields.Float('Max Tolerance', related='point_id.tolerance_max', readonly=True)
|
||||
warning_message = fields.Text(compute='_compute_warning_message')
|
||||
norm_unit = fields.Char(related='point_id.norm_unit', readonly=True)
|
||||
qty_to_test = fields.Float(compute="_compute_qty_to_test", string="Quantity to Test", help="Quantity of product to test within the lot")
|
||||
qty_tested = fields.Float(string="Quantity Tested", help="Quantity of product tested within the lot")
|
||||
measure_on = fields.Selection([
|
||||
('operation', 'Operation'),
|
||||
('product', 'Product'),
|
||||
('move_line', 'Quantity')], string="Control per", default='product', required=True,
|
||||
help="""Operation = One quality check is requested at the operation level.
|
||||
Product = A quality check is requested per product.
|
||||
Quantity = A quality check is requested for each new product quantity registered, with partial quantity checks also possible.""")
|
||||
move_line_id = fields.Many2one('stock.move.line', 'Stock Move Line', check_company=True, help="In case of Quality Check by Quantity, Move Line on which the Quality Check applies")
|
||||
lot_name = fields.Char('Lot/Serial Number Name')
|
||||
lot_line_id = fields.Many2one('stock.lot', store=True, compute='_compute_lot_line_id')
|
||||
qty_line = fields.Float(compute='_compute_qty_line', string="Quantity")
|
||||
uom_id = fields.Many2one(related='product_id.uom_id', string="Product Unit of Measure")
|
||||
show_lot_text = fields.Boolean(compute='_compute_show_lot_text')
|
||||
is_lot_tested_fractionally = fields.Boolean(related='point_id.is_lot_tested_fractionally')
|
||||
testing_percentage_within_lot = fields.Float(related="point_id.testing_percentage_within_lot")
|
||||
product_tracking = fields.Selection(related='product_id.tracking')
|
||||
|
||||
@api.depends('measure_success')
|
||||
def _compute_warning_message(self):
|
||||
for rec in self:
|
||||
if rec.measure_success == 'fail':
|
||||
rec.warning_message = _('You measured %.2f %s and it should be between %.2f and %.2f %s.') % (
|
||||
rec.measure, rec.norm_unit, rec.point_id.tolerance_min,
|
||||
rec.point_id.tolerance_max, rec.norm_unit
|
||||
)
|
||||
else:
|
||||
rec.warning_message = ''
|
||||
|
||||
@api.depends('move_line_id.qty_done')
|
||||
def _compute_qty_line(self):
|
||||
for qc in self:
|
||||
qc.qty_line = qc.move_line_id.qty_done
|
||||
|
||||
@api.depends('move_line_id.lot_id')
|
||||
def _compute_lot_line_id(self):
|
||||
for qc in self:
|
||||
qc.lot_line_id = qc.move_line_id.lot_id
|
||||
if qc.lot_line_id:
|
||||
qc.lot_id = qc.lot_line_id
|
||||
|
||||
@api.depends('measure')
|
||||
def _compute_measure_success(self):
|
||||
for rec in self:
|
||||
if rec.point_id.test_type == 'passfail':
|
||||
rec.measure_success = 'none'
|
||||
else:
|
||||
if rec.measure < rec.point_id.tolerance_min or rec.measure > rec.point_id.tolerance_max:
|
||||
rec.measure_success = 'fail'
|
||||
else:
|
||||
rec.measure_success = 'pass'
|
||||
|
||||
# Add picture dependency
|
||||
@api.depends('picture')
|
||||
def _compute_result(self):
|
||||
super(QualityCheck, self)._compute_result()
|
||||
|
||||
@api.depends('qty_line', 'testing_percentage_within_lot', 'is_lot_tested_fractionally')
|
||||
def _compute_qty_to_test(self):
|
||||
for qc in self:
|
||||
if qc.is_lot_tested_fractionally:
|
||||
qc.qty_to_test = float_round(qc.qty_line * qc.testing_percentage_within_lot / 100, precision_rounding=self.product_id.uom_id.rounding, rounding_method="UP")
|
||||
else:
|
||||
qc.qty_to_test = qc.qty_line
|
||||
|
||||
@api.depends('lot_line_id', 'move_line_id')
|
||||
def _compute_show_lot_text(self):
|
||||
for qc in self:
|
||||
if qc.lot_line_id or not qc.move_line_id:
|
||||
qc.show_lot_text = False
|
||||
else:
|
||||
qc.show_lot_text = True
|
||||
|
||||
def _is_pass_fail_applicable(self):
|
||||
if self.test_type in ['passfail', 'measure']:
|
||||
return True
|
||||
return super()._is_pass_fail_applicable()
|
||||
|
||||
def _get_check_result(self):
|
||||
if self.test_type == 'picture' and self.picture:
|
||||
return _('Picture Uploaded')
|
||||
else:
|
||||
return super(QualityCheck, self)._get_check_result()
|
||||
|
||||
def _check_to_unlink(self):
|
||||
return True
|
||||
|
||||
def do_measure(self):
|
||||
self.ensure_one()
|
||||
if self.measure < self.point_id.tolerance_min or self.measure > self.point_id.tolerance_max:
|
||||
return self.do_fail()
|
||||
else:
|
||||
return self.do_pass()
|
||||
|
||||
def correct_measure(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Quality Checks'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'quality.check',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('quality_control.quality_check_view_form_small').id,
|
||||
'target': 'new',
|
||||
'res_id': self.id,
|
||||
'context': self.env.context,
|
||||
}
|
||||
|
||||
def do_alert(self):
|
||||
self.ensure_one()
|
||||
alert = self.env['quality.alert'].create({
|
||||
'check_id': self.id,
|
||||
'product_id': self.product_id.id,
|
||||
'product_tmpl_id': self.product_id.product_tmpl_id.id,
|
||||
'lot_id': self.lot_id.id,
|
||||
'user_id': self.user_id.id,
|
||||
'team_id': self.team_id.id,
|
||||
'company_id': self.company_id.id
|
||||
})
|
||||
return {
|
||||
'name': _('Quality Alert'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'quality.alert',
|
||||
'views': [(self.env.ref('quality_control.quality_alert_view_form').id, 'form')],
|
||||
'res_id': alert.id,
|
||||
'context': {'default_check_id': self.id},
|
||||
}
|
||||
|
||||
def action_see_alerts(self):
|
||||
self.ensure_one()
|
||||
if len(self.alert_ids) == 1:
|
||||
return {
|
||||
'name': _('Quality Alert'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'quality.alert',
|
||||
'views': [(self.env.ref('quality_control.quality_alert_view_form').id, 'form')],
|
||||
'res_id': self.alert_ids.ids[0],
|
||||
'context': {'default_check_id': self.id},
|
||||
}
|
||||
else:
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_alert_action_check")
|
||||
action['domain'] = [('id', 'in', self.alert_ids.ids)]
|
||||
action['context'] = dict(self._context, default_check_id=self.id)
|
||||
return action
|
||||
|
||||
def action_open_quality_check_wizard(self, current_check_id=None):
|
||||
check_ids = sorted(self.ids)
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.action_quality_check_wizard")
|
||||
action['context'] = self.env.context.copy()
|
||||
action['context'].update({
|
||||
'default_check_ids': check_ids,
|
||||
'default_current_check_id': current_check_id or check_ids[0],
|
||||
})
|
||||
return action
|
||||
|
||||
|
||||
class QualityAlert(models.Model):
|
||||
_inherit = "quality.alert"
|
||||
|
||||
title = fields.Char('Title')
|
||||
|
||||
def action_see_check(self):
|
||||
return {
|
||||
'name': _('Quality Check'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'quality.check',
|
||||
'target': 'current',
|
||||
'res_id': self.check_id.id,
|
||||
}
|
||||
|
||||
@api.depends('name', 'title')
|
||||
def name_get(self):
|
||||
result = []
|
||||
for record in self:
|
||||
name = record.name + ' - ' + record.title if record.title else record.name
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def name_create(self, name):
|
||||
""" Create an alert with name_create should use prepend the sequence in the name """
|
||||
values = {
|
||||
'title': name,
|
||||
}
|
||||
return self.create(values).name_get()[0]
|
||||
|
||||
@api.model
|
||||
def message_new(self, msg_dict, custom_values=None):
|
||||
""" Override, used with creation by email alias. The purpose of the override is
|
||||
to use the subject for title and body for description instead of the name.
|
||||
"""
|
||||
# We need to add the name in custom_values or it will use the subject.
|
||||
custom_values['name'] = self.env['ir.sequence'].next_by_code('quality.alert') or _('New')
|
||||
if msg_dict.get('subject'):
|
||||
custom_values['title'] = msg_dict['subject']
|
||||
if msg_dict.get('body'):
|
||||
custom_values['description'] = msg_dict['body']
|
||||
return super(QualityAlert, self).message_new(msg_dict, custom_values)
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
|
||||
quality_pass_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
|
||||
quality_fail_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
|
||||
|
||||
@api.depends('product_variant_ids')
|
||||
def _compute_quality_check_qty(self):
|
||||
for product_tmpl in self:
|
||||
product_tmpl.quality_fail_qty, product_tmpl.quality_pass_qty = product_tmpl.product_variant_ids._count_quality_checks()
|
||||
product_tmpl.quality_control_point_qty = product_tmpl.with_context(active_test=product_tmpl.active).product_variant_ids._count_quality_points()
|
||||
|
||||
def action_see_quality_control_points(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_point_action")
|
||||
action['context'] = dict(self.env.context, default_product_ids=self.product_variant_ids.ids)
|
||||
|
||||
domain_in_products_or_categs = ['|', ('product_ids', 'in', self.product_variant_ids.ids), ('product_category_ids', 'parent_of', self.categ_id.ids)]
|
||||
domain_no_products_and_categs = [('product_ids', '=', False), ('product_category_ids', '=', False)]
|
||||
action['domain'] = OR([domain_in_products_or_categs, domain_no_products_and_categs])
|
||||
return action
|
||||
|
||||
def action_see_quality_checks(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_check_action_main")
|
||||
action['context'] = dict(self.env.context, default_product_id=self.product_variant_id.id, create=False)
|
||||
action['domain'] = [
|
||||
'|',
|
||||
('product_id', 'in', self.product_variant_ids.ids),
|
||||
'&',
|
||||
('measure_on', '=', 'operation'),
|
||||
('picking_id.move_ids.product_tmpl_id', '=', self.id),
|
||||
]
|
||||
return action
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = "product.product"
|
||||
|
||||
quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
|
||||
quality_pass_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
|
||||
quality_fail_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user')
|
||||
|
||||
def _compute_quality_check_qty(self):
|
||||
for product in self:
|
||||
product.quality_fail_qty, product.quality_pass_qty = product._count_quality_checks()
|
||||
product.quality_control_point_qty = product._count_quality_points()
|
||||
|
||||
def _count_quality_checks(self):
|
||||
quality_fail_qty = 0
|
||||
quality_pass_qty = 0
|
||||
domain = [
|
||||
'|',
|
||||
('product_id', 'in', self.ids),
|
||||
'&',
|
||||
('measure_on', '=', 'operation'),
|
||||
('picking_id.move_ids.product_id', 'in', self.ids),
|
||||
('company_id', '=', self.env.company.id),
|
||||
('quality_state', '!=', 'none')
|
||||
]
|
||||
quality_checks_by_state = self.env['quality.check']._read_group(domain, ['product_id'], ['quality_state'])
|
||||
for checks_data in quality_checks_by_state:
|
||||
if checks_data['quality_state'] == 'fail':
|
||||
quality_fail_qty = checks_data['quality_state_count']
|
||||
elif checks_data['quality_state'] == 'pass':
|
||||
quality_pass_qty = checks_data['quality_state_count']
|
||||
|
||||
return quality_fail_qty, quality_pass_qty
|
||||
|
||||
def _count_quality_points(self):
|
||||
""" Compute the count of all related quality points, which means quality points that have either
|
||||
the product in common, a product category parent of this product's category or no product/category
|
||||
set at all.
|
||||
"""
|
||||
|
||||
query = self.env['quality.point']._where_calc([('company_id', '=', self.env.company.id)])
|
||||
self.env['quality.point']._apply_ir_rules(query, 'read')
|
||||
_, where_clause, where_clause_args = query.get_sql()
|
||||
additional_where_clause = self._additional_quality_point_where_clause()
|
||||
where_clause += additional_where_clause
|
||||
parent_category_ids = [int(parent_id) for parent_id in self.categ_id.parent_path.split('/')[:-1]] if self.categ_id else []
|
||||
|
||||
self.env.cr.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM quality_point
|
||||
WHERE %s
|
||||
AND (
|
||||
(
|
||||
-- QP has at least one linked product and one is right
|
||||
EXISTS (SELECT 1 FROM product_product_quality_point_rel rel WHERE rel.quality_point_id = quality_point.id AND rel.product_product_id = ANY(%%s))
|
||||
-- Or QP has at least one linked product category and one is right
|
||||
OR EXISTS (SELECT 1 FROM product_category_quality_point_rel rel WHERE rel.quality_point_id = quality_point.id AND rel.product_category_id = ANY(%%s))
|
||||
)
|
||||
OR (
|
||||
-- QP has no linked products
|
||||
NOT EXISTS (SELECT 1 FROM product_product_quality_point_rel rel WHERE rel.quality_point_id = quality_point.id)
|
||||
-- And QP has no linked product categories
|
||||
AND NOT EXISTS (SELECT 1 FROM product_category_quality_point_rel rel WHERE rel.quality_point_id = quality_point.id)
|
||||
)
|
||||
)
|
||||
""" % (where_clause,), where_clause_args + [self.ids, parent_category_ids]
|
||||
)
|
||||
return self.env.cr.fetchone()[0]
|
||||
|
||||
def action_see_quality_control_points(self):
|
||||
self.ensure_one()
|
||||
action = self.product_tmpl_id.action_see_quality_control_points()
|
||||
action['context'].update(default_product_ids=self.ids)
|
||||
|
||||
domain_in_products_or_categs = ['|', ('product_ids', 'in', self.ids), ('product_category_ids', 'parent_of', self.categ_id.ids)]
|
||||
domain_no_products_and_categs = [('product_ids', '=', False), ('product_category_ids', '=', False)]
|
||||
action['domain'] = OR([domain_in_products_or_categs, domain_no_products_and_categs])
|
||||
return action
|
||||
|
||||
def action_see_quality_checks(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_check_action_main")
|
||||
action['context'] = dict(self.env.context, default_product_id=self.id, create=False)
|
||||
action['domain'] = [
|
||||
'|',
|
||||
('product_id', '=', self.id),
|
||||
'&',
|
||||
('measure_on', '=', 'operation'),
|
||||
('picking_id.move_ids.product_id', '=', self.id),
|
||||
]
|
||||
return action
|
||||
|
||||
def _additional_quality_point_where_clause(self):
|
||||
return ""
|
||||
18
quality_control/models/stock_lot.py
Normal file
18
quality_control/models/stock_lot.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductionLot(models.Model):
|
||||
_inherit = 'stock.lot'
|
||||
|
||||
quality_check_qty = fields.Integer(
|
||||
compute='_compute_quality_check_qty', groups='quality.group_quality_user')
|
||||
|
||||
def _compute_quality_check_qty(self):
|
||||
for prod_lot in self:
|
||||
prod_lot.quality_check_qty = self.env['quality.check'].search_count([
|
||||
('lot_id', '=', prod_lot.id),
|
||||
('company_id', '=', self.env.company.id)
|
||||
])
|
||||
69
quality_control/models/stock_move.py
Normal file
69
quality_control/models/stock_move.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
def _action_confirm(self, merge=True, merge_into=False):
|
||||
moves = super(StockMove, self)._action_confirm(merge=merge, merge_into=merge_into)
|
||||
moves._create_quality_checks()
|
||||
return moves
|
||||
|
||||
def _create_quality_checks(self):
|
||||
# Groupby move by picking. Use it in order to generate missing quality checks.
|
||||
pick_moves = defaultdict(lambda: self.env['stock.move'])
|
||||
for move in self:
|
||||
if move.picking_id:
|
||||
pick_moves[move.picking_id] |= move
|
||||
check_vals_list = self._create_operation_quality_checks(pick_moves)
|
||||
for picking, moves in pick_moves.items():
|
||||
# Quality checks by product
|
||||
quality_points_domain = self.env['quality.point']._get_domain(moves.product_id, picking.picking_type_id, measure_on='product')
|
||||
quality_points = self.env['quality.point'].sudo().search(quality_points_domain)
|
||||
|
||||
if not quality_points:
|
||||
continue
|
||||
picking_check_vals_list = quality_points._get_checks_values(moves.product_id, picking.company_id.id, existing_checks=picking.sudo().check_ids)
|
||||
for check_value in picking_check_vals_list:
|
||||
check_value.update({
|
||||
'picking_id': picking.id,
|
||||
})
|
||||
check_vals_list += picking_check_vals_list
|
||||
self.env['quality.check'].sudo().create(check_vals_list)
|
||||
|
||||
def _create_operation_quality_checks(self, pick_moves):
|
||||
check_vals_list = []
|
||||
for picking, moves in pick_moves.items():
|
||||
quality_points_domain = self.env['quality.point']._get_domain(moves.product_id, picking.picking_type_id, measure_on='operation')
|
||||
quality_points = self.env['quality.point'].sudo().search(quality_points_domain)
|
||||
for point in quality_points:
|
||||
if point.check_execute_now():
|
||||
check_vals_list.append({
|
||||
'point_id': point.id,
|
||||
'team_id': point.team_id.id,
|
||||
'measure_on': 'operation',
|
||||
'picking_id': picking.id,
|
||||
})
|
||||
return check_vals_list
|
||||
|
||||
def _action_cancel(self):
|
||||
res = super()._action_cancel()
|
||||
|
||||
to_unlink = self.env['quality.check'].sudo()
|
||||
is_product_canceled = defaultdict(lambda: True)
|
||||
for qc in self.picking_id.sudo().check_ids:
|
||||
if qc.quality_state != 'none':
|
||||
continue
|
||||
if (qc.picking_id, qc.product_id) not in is_product_canceled:
|
||||
for move in qc.picking_id.move_ids:
|
||||
is_product_canceled[(move.picking_id, move.product_id)] &= move.state == 'cancel'
|
||||
if is_product_canceled[(qc.picking_id, qc.product_id)]:
|
||||
to_unlink |= qc
|
||||
to_unlink.unlink()
|
||||
|
||||
return res
|
||||
102
quality_control/models/stock_move_line.py
Normal file
102
quality_control/models/stock_move_line.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = "stock.move.line"
|
||||
|
||||
check_ids = fields.One2many('quality.check', 'move_line_id', 'Checks')
|
||||
check_state = fields.Selection([
|
||||
('no_checks', 'No checks'),
|
||||
('in_progress', 'Some checks to be done'),
|
||||
('pass', 'All checks passed'),
|
||||
('fail', 'Some checks failed')], compute="_compute_check_state")
|
||||
|
||||
@api.depends('check_ids')
|
||||
def _compute_check_state(self):
|
||||
for line in self:
|
||||
if not line.check_ids:
|
||||
line.check_state = 'no_checks'
|
||||
elif line.check_ids.filtered(lambda check: check.quality_state == 'none'):
|
||||
line.check_state = 'in_progress'
|
||||
elif line.check_ids.filtered(lambda check: check.quality_state == 'fail'):
|
||||
line.check_state = "fail"
|
||||
else:
|
||||
line.check_state = "pass"
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
lines = super().create(vals_list)
|
||||
lines._filter_move_lines_applicable_for_quality_check()._create_check()
|
||||
return lines
|
||||
|
||||
def write(self, vals):
|
||||
if self._create_quality_check_at_write(vals):
|
||||
self.filtered(lambda ml: not ml.qty_done)._create_check()
|
||||
return super().write(vals)
|
||||
|
||||
def unlink(self):
|
||||
self.sudo()._unlink_quality_check()
|
||||
return super(StockMoveLine, self).unlink()
|
||||
|
||||
def action_open_quality_check_wizard(self):
|
||||
return self.check_ids.action_open_quality_check_wizard()
|
||||
|
||||
def _unlink_quality_check(self):
|
||||
self.check_ids.filtered(lambda qc: qc._check_to_unlink()).unlink()
|
||||
|
||||
def _create_quality_check_at_write(self, vals):
|
||||
return vals.get('qty_done')
|
||||
|
||||
def _create_check(self):
|
||||
check_values_list = []
|
||||
quality_points_domain = self.env['quality.point']._get_domain(
|
||||
self.product_id, self.move_id.picking_type_id, measure_on='move_line')
|
||||
quality_points = self.env['quality.point'].sudo().search(quality_points_domain)
|
||||
quality_points_by_product_picking_type = {}
|
||||
for quality_point in quality_points:
|
||||
for product in quality_point.product_ids:
|
||||
for picking_type in quality_point.picking_type_ids:
|
||||
quality_points_by_product_picking_type.setdefault(
|
||||
(product, picking_type), set()).add(quality_point.id)
|
||||
for categ in quality_point.product_category_ids:
|
||||
categ_product = self.env['product.product'].search([
|
||||
('categ_id', 'child_of', categ.id)
|
||||
])
|
||||
for product in categ_product & self.product_id:
|
||||
for picking_type in quality_point.picking_type_ids:
|
||||
quality_points_by_product_picking_type.setdefault(
|
||||
(product, picking_type), set()).add(quality_point.id)
|
||||
if not quality_point.product_ids:
|
||||
for picking_type in quality_point.picking_type_ids:
|
||||
quality_points_by_product_picking_type.setdefault(
|
||||
(None, picking_type), set()).add(quality_point.id)
|
||||
|
||||
for ml in self:
|
||||
quality_points_product = quality_points_by_product_picking_type.get((ml.product_id, ml.move_id.picking_type_id), set())
|
||||
quality_points_all_products = ml._get_quality_points_all_products(quality_points_by_product_picking_type)
|
||||
quality_points = self.env['quality.point'].sudo().search([('id', 'in', list(quality_points_product | quality_points_all_products))])
|
||||
for quality_point in quality_points:
|
||||
if quality_point.check_execute_now():
|
||||
check_values = ml._get_check_values(quality_point)
|
||||
check_values_list.append(check_values)
|
||||
if check_values_list:
|
||||
self.env['quality.check'].sudo().create(check_values_list)
|
||||
|
||||
def _filter_move_lines_applicable_for_quality_check(self):
|
||||
return self.filtered(lambda line: line.qty_done != 0)
|
||||
|
||||
def _get_check_values(self, quality_point):
|
||||
return {
|
||||
'point_id': quality_point.id,
|
||||
'measure_on': quality_point.measure_on,
|
||||
'team_id': quality_point.team_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'picking_id': self.picking_id.id,
|
||||
'move_line_id': self.id,
|
||||
'lot_name': self.lot_name,
|
||||
}
|
||||
|
||||
def _get_quality_points_all_products(self, quality_points_by_product_picking_type):
|
||||
return quality_points_by_product_picking_type.get((None, self.move_id.picking_type_id), set())
|
||||
122
quality_control/models/stock_picking.py
Normal file
122
quality_control/models/stock_picking.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
check_ids = fields.One2many('quality.check', 'picking_id', 'Checks')
|
||||
quality_check_todo = fields.Boolean('Pending checks', compute='_compute_check')
|
||||
quality_check_fail = fields.Boolean(compute='_compute_check')
|
||||
quality_alert_ids = fields.One2many('quality.alert', 'picking_id', 'Alerts')
|
||||
quality_alert_count = fields.Integer(compute='_compute_quality_alert_count')
|
||||
|
||||
def _compute_check(self):
|
||||
for picking in self:
|
||||
todo = False
|
||||
fail = False
|
||||
checkable_products = picking.mapped('move_line_ids').mapped('product_id')
|
||||
for check in picking.check_ids:
|
||||
if check.quality_state == 'none' and (check.product_id in checkable_products or check.measure_on == 'operation'):
|
||||
todo = True
|
||||
elif check.quality_state == 'fail':
|
||||
fail = True
|
||||
if fail and todo:
|
||||
break
|
||||
picking.quality_check_fail = fail
|
||||
picking.quality_check_todo = todo
|
||||
|
||||
def _compute_quality_alert_count(self):
|
||||
for picking in self:
|
||||
picking.quality_alert_count = len(picking.quality_alert_ids)
|
||||
|
||||
@api.depends('quality_check_todo')
|
||||
def _compute_show_validate(self):
|
||||
super()._compute_show_validate()
|
||||
for picking in self:
|
||||
if picking.quality_check_todo:
|
||||
picking.show_validate = False
|
||||
|
||||
def check_quality(self):
|
||||
self.ensure_one()
|
||||
checkable_products = self.mapped('move_line_ids').mapped('product_id')
|
||||
checks = self.check_ids.filtered(lambda check: check.quality_state == 'none' and (check.product_id in checkable_products or check.measure_on == 'operation'))
|
||||
if checks:
|
||||
return checks.action_open_quality_check_wizard()
|
||||
return False
|
||||
|
||||
def _create_backorder(self):
|
||||
res = super(StockPicking, self)._create_backorder()
|
||||
if self.env.context.get('skip_check'):
|
||||
return res
|
||||
for backorder in res:
|
||||
backorder.backorder_id.check_ids.filtered(lambda qc: qc.quality_state == 'none').unlink()
|
||||
backorder.move_ids._create_quality_checks()
|
||||
return res
|
||||
|
||||
def _action_done(self):
|
||||
# Do the check before transferring
|
||||
product_to_check = self.mapped('move_line_ids').filtered(lambda x: x.qty_done != 0).mapped('product_id')
|
||||
if self.mapped('check_ids').filtered(lambda x: x.quality_state == 'none' and x.product_id in product_to_check):
|
||||
raise UserError(_('You still need to do the quality checks!'))
|
||||
return super(StockPicking, self)._action_done()
|
||||
|
||||
def _pre_action_done_hook(self):
|
||||
res = super()._pre_action_done_hook()
|
||||
if res is True:
|
||||
pickings_to_check_quality = self._check_for_quality_checks()
|
||||
if pickings_to_check_quality:
|
||||
return pickings_to_check_quality[0].with_context(pickings_to_check_quality=pickings_to_check_quality.ids).check_quality()
|
||||
return res
|
||||
|
||||
def _check_for_quality_checks(self):
|
||||
quality_pickings = self.env['stock.picking']
|
||||
for picking in self:
|
||||
product_to_check = picking.mapped('move_line_ids').filtered(lambda ml: ml.qty_done != 0).mapped('product_id')
|
||||
if picking.mapped('check_ids').filtered(lambda qc: qc.quality_state == 'none' and qc.product_id in product_to_check):
|
||||
quality_pickings |= picking
|
||||
return quality_pickings
|
||||
|
||||
def action_cancel(self):
|
||||
res = super(StockPicking, self).action_cancel()
|
||||
self.sudo().mapped('check_ids').filtered(lambda x: x.quality_state == 'none').unlink()
|
||||
return res
|
||||
|
||||
def action_open_quality_check_picking(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_check_action_picking")
|
||||
action['context'] = self.env.context.copy()
|
||||
action['context'].update({
|
||||
'search_default_picking_id': [self.id],
|
||||
'default_picking_id': self.id,
|
||||
'show_lots_text': self.show_lots_text,
|
||||
})
|
||||
return action
|
||||
|
||||
def button_quality_alert(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_alert_action_check")
|
||||
action['views'] = [(False, 'form')]
|
||||
action['context'] = {
|
||||
'default_product_id': self.product_id.id,
|
||||
'default_product_tmpl_id': self.product_id.product_tmpl_id.id,
|
||||
'default_picking_id': self.id,
|
||||
}
|
||||
return action
|
||||
|
||||
def open_quality_alert_picking(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_alert_action_check")
|
||||
action['context'] = {
|
||||
'default_product_id': self.product_id.id,
|
||||
'default_product_tmpl_id': self.product_id.product_tmpl_id.id,
|
||||
'default_picking_id': self.id,
|
||||
}
|
||||
action['domain'] = [('id', 'in', self.quality_alert_ids.ids)]
|
||||
action['views'] = [(False, 'tree'),(False,'form')]
|
||||
if self.quality_alert_count == 1:
|
||||
action['views'] = [(False, 'form')]
|
||||
action['res_id'] = self.quality_alert_ids.id
|
||||
return action
|
||||
Reference in New Issue
Block a user