质量模块和库存扫码

This commit is contained in:
qihao.gong@jikimo.com
2023-07-24 11:42:15 +08:00
parent 8d024ad625
commit 3c89404543
228 changed files with 142596 additions and 0 deletions

View 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

View 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 Welfords 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 ""

View 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)
])

View 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

View 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())

View 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