合并企业版代码(未测试,先提交到测试分支)

This commit is contained in:
qihao.gong@jikimo.com
2023-04-14 17:42:23 +08:00
parent 7a7b3d7126
commit d28525526a
1300 changed files with 513579 additions and 5426 deletions

View File

@@ -0,0 +1,10 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_production
from . import mrp_workorder
from . import quality
from . import res_config_settings
from . import stock_move
from . import stock_move_line
from . import mrp_bom

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class MrpBom(models.Model):
_name = 'mrp.bom'
_inherit = ['mail.activity.mixin', 'mrp.bom']
def write(self, vals):
res = super().write(vals)
if 'product_id' in vals or 'product_tmpl_id' in vals:
self.operation_ids.quality_point_ids._change_product_ids_for_bom(self)
return res

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class MrpProduction(models.Model):
_inherit = 'mrp.production'
_start_name = "date_planned_start"
_stop_name = "date_planned_finished"
check_ids = fields.One2many('quality.check', 'production_id', string="Checks")
def _split_productions(self, amounts=False, cancel_remaining_qty=False, set_consumed_qty=False):
productions = super()._split_productions(amounts=amounts, cancel_remaining_qty=cancel_remaining_qty, set_consumed_qty=set_consumed_qty)
backorders = productions[1:]
if not backorders:
return productions
for wo in backorders.workorder_ids:
if wo.current_quality_check_id.component_id:
wo.current_quality_check_id._update_component_quantity()
return productions

View File

@@ -0,0 +1,631 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from bisect import bisect_left
from collections import defaultdict
from datetime import datetime
from pytz import utc
from odoo import api, fields, models, _
from odoo.addons.web.controllers.utils import clean_action
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare, float_is_zero, relativedelta
from odoo.addons.resource.models.resource import Intervals, sum_intervals, string_to_datetime
class MrpWorkcenter(models.Model):
_name = 'mrp.workcenter'
_inherit = 'mrp.workcenter'
def action_work_order(self):
if not self.env.context.get('desktop_list_view', False):
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.mrp_workorder_action_tablet")
return action
else:
return super(MrpWorkcenter, self).action_work_order()
class MrpProductionWorkcenterLine(models.Model):
_name = 'mrp.workorder'
_inherit = ['mrp.workorder', 'barcodes.barcode_events_mixin']
quality_point_ids = fields.Many2many('quality.point', compute='_compute_quality_point_ids', store=True)
quality_point_count = fields.Integer('Steps', compute='_compute_quality_point_count')
check_ids = fields.One2many('quality.check', 'workorder_id')
finished_product_check_ids = fields.Many2many('quality.check', compute='_compute_finished_product_check_ids')
quality_check_todo = fields.Boolean(compute='_compute_check')
quality_check_fail = fields.Boolean(compute='_compute_check')
quality_alert_ids = fields.One2many('quality.alert', 'workorder_id')
quality_alert_count = fields.Integer(compute="_compute_quality_alert_count")
current_quality_check_id = fields.Many2one(
'quality.check', "Current Quality Check", check_company=True)
# QC-related fields
allow_producing_quantity_change = fields.Boolean('Allow Changes to Producing Quantity', default=True)
is_last_lot = fields.Boolean('Is Last lot', compute='_compute_is_last_lot')
is_first_started_wo = fields.Boolean('Is The first Work Order', compute='_compute_is_last_unfinished_wo')
is_last_unfinished_wo = fields.Boolean('Is Last Work Order To Process', compute='_compute_is_last_unfinished_wo', store=False)
lot_id = fields.Many2one(related='current_quality_check_id.lot_id', readonly=False)
move_id = fields.Many2one(related='current_quality_check_id.move_id', readonly=False)
move_line_id = fields.Many2one(related='current_quality_check_id.move_line_id', readonly=False)
move_line_ids = fields.One2many(related='move_id.move_line_ids')
quality_state = fields.Selection(related='current_quality_check_id.quality_state', string="Quality State", readonly=False)
qty_done = fields.Float(related='current_quality_check_id.qty_done', readonly=False)
test_type_id = fields.Many2one('quality.point.test_type', 'Test Type', related='current_quality_check_id.test_type_id')
test_type = fields.Char(related='test_type_id.technical_name')
user_id = fields.Many2one(related='current_quality_check_id.user_id', readonly=False)
worksheet_page = fields.Integer('Worksheet page')
picture = fields.Binary(related='current_quality_check_id.picture', readonly=False)
additional = fields.Boolean(related='current_quality_check_id.additional')
@api.depends('operation_id')
def _compute_quality_point_ids(self):
for workorder in self:
quality_points = workorder.operation_id.quality_point_ids
quality_points = quality_points.filtered(lambda qp: not qp.product_ids or workorder.production_id.product_id in qp.product_ids)
workorder.quality_point_ids = quality_points
@api.depends('operation_id')
def _compute_quality_point_count(self):
for workorder in self:
quality_point = workorder.operation_id.quality_point_ids
workorder.quality_point_count = len(quality_point)
@api.depends('qty_producing', 'qty_remaining')
def _compute_is_last_lot(self):
for wo in self:
precision = wo.production_id.product_uom_id.rounding
wo.is_last_lot = float_compare(wo.qty_producing, wo.qty_remaining, precision_rounding=precision) >= 0
@api.depends('production_id.workorder_ids')
def _compute_is_last_unfinished_wo(self):
for wo in self:
wo.is_first_started_wo = all(wo.state != 'done' for wo in (wo.production_id.workorder_ids - wo))
other_wos = wo.production_id.workorder_ids - wo
other_states = other_wos.mapped(lambda w: w.state == 'done')
wo.is_last_unfinished_wo = all(other_states)
@api.depends('check_ids')
def _compute_finished_product_check_ids(self):
for wo in self:
wo.finished_product_check_ids = wo.check_ids.filtered(lambda c: c.finished_product_sequence == wo.qty_produced)
def write(self, values):
res = super().write(values)
if 'qty_producing' in values:
for wo in self:
if wo.current_quality_check_id.component_id:
wo.current_quality_check_id._update_component_quantity()
return res
def action_back(self):
self.ensure_one()
if self.is_user_working and self.working_state != 'blocked':
self.button_pending()
domain = [('state', 'not in', ['done', 'cancel', 'pending'])]
if self.env.context.get('from_production_order'):
action = self.env["ir.actions.actions"]._for_xml_id("mrp.action_mrp_workorder_production_specific")
action['domain'] = domain
action['target'] = 'main'
action['view_id'] = 'mrp.mrp_production_workorder_tree_editable_view'
action['context'] = {
'no_breadcrumbs': True,
}
if self.env.context.get('from_manufacturing_order'):
action['context'].update({
'search_default_production_id': self.production_id.id
})
else:
# workorder tablet view action should redirect to the same tablet view with same workcenter when WO mark as done.
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.mrp_workorder_action_tablet")
action['domain'] = domain
action['context'] = {
'no_breadcrumbs': True,
'search_default_workcenter_id': self.workcenter_id.id
}
return clean_action(action, self.env)
def action_cancel(self):
self.mapped('check_ids').filtered(lambda c: c.quality_state == 'none').sudo().unlink()
return super(MrpProductionWorkcenterLine, self).action_cancel()
def action_generate_serial(self):
self.ensure_one()
self.finished_lot_id = self.env['stock.lot'].create({
'product_id': self.product_id.id,
'company_id': self.company_id.id,
'name': self.env['stock.lot']._get_next_serial(self.company_id, self.product_id) or self.env['ir.sequence'].next_by_code('stock.lot.serial'),
})
def _create_subsequent_checks(self):
""" When processing a step with regiter a consumed material
that's a lot we will some times need to create a new
intermediate check.
e.g.: Register 2 product A tracked by SN. We will register one
with the current checks but we need to generate a second step
for the second SN. Same for lot if the user wants to use more
than one lot.
"""
# Create another quality check if necessary
next_check = self.current_quality_check_id.next_check_id
if next_check.component_id != self.current_quality_check_id.product_id or\
next_check.point_id != self.current_quality_check_id.point_id:
# TODO: manage reservation here
# Creating quality checks
quality_check_data = {
'workorder_id': self.id,
'product_id': self.product_id.id,
'company_id': self.company_id.id,
'finished_product_sequence': self.qty_produced,
}
if self.current_quality_check_id.point_id:
quality_check_data.update({
'point_id': self.current_quality_check_id.point_id.id,
'team_id': self.current_quality_check_id.point_id.team_id.id,
})
else:
quality_check_data.update({
'component_id': self.current_quality_check_id.component_id.id,
'test_type_id': self.current_quality_check_id.test_type_id.id,
'team_id': self.current_quality_check_id.team_id.id,
})
move = self.current_quality_check_id.move_id
quality_check_data.update(self._defaults_from_move(move))
new_check = self.env['quality.check'].create(quality_check_data)
new_check._insert_in_chain('after', self.current_quality_check_id)
def _change_quality_check(self, position):
"""Change the quality check currently set on the workorder `self`.
The workorder points to a check. A check belongs to a chain.
This method allows to change the selected check by moving on the checks
chain according to `position`.
:param position: Where we need to change the cursor on the check chain
:type position: string
"""
self.ensure_one()
assert position in ['first', 'next', 'previous', 'last']
checks_to_consider = self.check_ids.filtered(lambda c: c.quality_state == 'none')
if position == 'first':
check = checks_to_consider.filtered(lambda check: not check.previous_check_id)
elif position == 'next':
check = self.current_quality_check_id.next_check_id
if not check:
check = checks_to_consider[:1]
elif check.quality_state != 'none':
self.current_quality_check_id = check
return self._change_quality_check(position='next')
if check.test_type in ('register_byproducts', 'register_consumed_materials'):
check._update_component_quantity()
elif position == 'previous':
check = self.current_quality_check_id.previous_check_id
else:
check = checks_to_consider.filtered(lambda check: not check.next_check_id)
self.write({
'allow_producing_quantity_change':
not check.previous_check_id.filtered(lambda c: c.quality_state != 'fail')
and all(c.quality_state != 'fail' for c in checks_to_consider)
and self.is_first_started_wo,
'current_quality_check_id': check.id,
'worksheet_page': check.point_id.worksheet_page,
})
def action_menu(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'mrp.workorder',
'views': [[self.env.ref('mrp_workorder.mrp_workorder_view_form_tablet_menu').id, 'form']],
'name': _('Menu'),
'target': 'new',
'res_id': self.id,
}
def action_add_component(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'mrp_workorder.additional.product',
'views': [[self.env.ref('mrp_workorder.view_mrp_workorder_additional_product_wizard').id, 'form']],
'name': _('Add Component'),
'target': 'new',
'context': {
'default_workorder_id': self.id,
'default_type': 'component',
'default_company_id': self.company_id.id,
}
}
def action_add_byproduct(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'mrp_workorder.additional.product',
'views': [[self.env.ref('mrp_workorder.view_mrp_workorder_additional_product_wizard').id, 'form']],
'name': _('Add By-Product'),
'target': 'new',
'context': {
'default_workorder_id': self.id,
'default_type': 'byproduct',
}
}
def button_start(self):
res = super().button_start()
for check in self.check_ids:
if check.component_tracking == 'serial' and check.component_id:
check._update_component_quantity()
return res
def action_propose_change(self, change_type, title):
return {
'type': 'ir.actions.act_window',
'res_model': 'propose.change',
'views': [[self.env.ref('mrp_workorder.view_propose_change_wizard').id, 'form']],
'name': title,
'target': 'new',
'context': {
'default_workorder_id': self.id,
'default_step_id': self.current_quality_check_id.id,
'default_change_type': change_type,
}
}
def action_add_step(self):
self.ensure_one()
if self.current_quality_check_id:
team = self.current_quality_check_id.team_id
else:
team = self.env['quality.alert.team'].search(['|', ('company_id', '=', self.company_id.id), ('company_id', '=', False)], limit=1)
return {
'type': 'ir.actions.act_window',
'res_model': 'quality.check',
'views': [[self.env.ref('mrp_workorder.add_quality_check_from_tablet').id, 'form']],
'name': _('Add a Step'),
'target': 'new',
'context': {
'default_test_type_id': self.env.ref('quality.test_type_instructions').id,
'default_workorder_id': self.id,
'default_product_id': self.product_id.id,
'default_team_id': team.id,
}
}
def _compute_check(self):
for workorder in self:
todo = False
fail = False
for check in workorder.check_ids:
if check.quality_state == 'none':
todo = True
elif check.quality_state == 'fail':
fail = True
if fail and todo:
break
workorder.quality_check_fail = fail
workorder.quality_check_todo = todo
def _compute_quality_alert_count(self):
for workorder in self:
workorder.quality_alert_count = len(workorder.quality_alert_ids)
def _create_checks(self):
for wo in self:
# Track components which have a control point
processed_move = self.env['stock.move']
production = wo.production_id
move_raw_ids = wo.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
move_finished_ids = wo.move_finished_ids.filtered(lambda m: m.state not in ('done', 'cancel') and m.product_id != wo.production_id.product_id)
previous_check = self.env['quality.check']
for point in wo.quality_point_ids:
# Check if we need a quality control for this point
if point.check_execute_now():
moves = self.env['stock.move']
values = {
'production_id': production.id,
'workorder_id': wo.id,
'point_id': point.id,
'team_id': point.team_id.id,
'company_id': wo.company_id.id,
'product_id': production.product_id.id,
# Two steps are from the same production
# if and only if the produced quantities at the time they were created are equal.
'finished_product_sequence': wo.qty_produced,
'previous_check_id': previous_check.id,
'worksheet_document': point.worksheet_document,
}
if point.test_type == 'register_byproducts':
moves = move_finished_ids.filtered(lambda m: m.product_id == point.component_id)
if not moves:
moves = production.move_finished_ids.filtered(lambda m: not m.operation_id and m.product_id == point.component_id)
elif point.test_type == 'register_consumed_materials':
moves = move_raw_ids.filtered(lambda m: m.product_id == point.component_id)
if not moves:
moves = production.move_raw_ids.filtered(lambda m: not m.operation_id and m.product_id == point.component_id)
else:
check = self.env['quality.check'].create(values)
previous_check.next_check_id = check
previous_check = check
# Create 'register ...' checks
for move in moves:
check_vals = values.copy()
check_vals.update(wo._defaults_from_move(move))
# Create quality check and link it to the chain
check_vals.update({'previous_check_id': previous_check.id})
check = self.env['quality.check'].create(check_vals)
previous_check.next_check_id = check
previous_check = check
processed_move |= moves
# Generate quality checks associated with unreferenced components
moves_without_check = ((move_raw_ids | move_finished_ids) - processed_move).filtered(lambda move: (move.has_tracking != 'none' and not move.raw_material_production_id.use_auto_consume_components_lots) or move.operation_id)
quality_team_id = self.env['quality.alert.team'].search(['|', ('company_id', '=', wo.company_id.id), ('company_id', '=', False)], limit=1).id
for move in moves_without_check:
values = {
'production_id': production.id,
'workorder_id': wo.id,
'product_id': production.product_id.id,
'company_id': wo.company_id.id,
'component_id': move.product_id.id,
'team_id': quality_team_id,
# Two steps are from the same production
# if and only if the produced quantities at the time they were created are equal.
'finished_product_sequence': wo.qty_produced,
'previous_check_id': previous_check.id,
}
if move in move_raw_ids:
test_type = self.env.ref('mrp_workorder.test_type_register_consumed_materials')
if move in move_finished_ids:
test_type = self.env.ref('mrp_workorder.test_type_register_byproducts')
values.update({'test_type_id': test_type.id})
values.update(wo._defaults_from_move(move))
check = self.env['quality.check'].create(values)
previous_check.next_check_id = check
previous_check = check
# Set default quality_check
wo._change_quality_check(position='first')
def _get_byproduct_move_to_update(self):
moves = super(MrpProductionWorkcenterLine, self)._get_byproduct_move_to_update()
return moves.filtered(lambda m: m.product_id.tracking == 'none')
def record_production(self):
if not self:
return True
self.ensure_one()
self._check_sn_uniqueness()
self._check_company()
if any(x.quality_state == 'none' for x in self.check_ids if x.test_type != 'instructions'):
raise UserError(_('You still need to do the quality checks!'))
if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.'))
if self.production_id.product_id.tracking != 'none' and not self.finished_lot_id and self.move_raw_ids:
raise UserError(_('You should provide a lot/serial number for the final product'))
backorder = False
# Trigger the backorder process if we produce less than expected
if float_compare(self.qty_producing, self.qty_remaining, precision_rounding=self.product_uom_id.rounding) == -1 and self.is_first_started_wo:
backorder = self.production_id._split_productions()[1:]
for workorder in backorder.workorder_ids:
if workorder.product_tracking == 'serial':
workorder.qty_producing = 1
else:
workorder.qty_producing = workorder.qty_remaining
self.production_id.product_qty = self.qty_producing
else:
if self.operation_id:
backorder = (self.production_id.procurement_group_id.mrp_production_ids - self.production_id).filtered(
lambda p: p.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_id).state not in ('cancel', 'done')
)[:1]
else:
index = list(self.production_id.workorder_ids).index(self)
backorder = (self.production_id.procurement_group_id.mrp_production_ids - self.production_id).filtered(
lambda p: index < len(p.workorder_ids) and p.workorder_ids[index].state not in ('cancel', 'done')
)[:1]
self.button_finish()
if backorder:
for wo in (self.production_id | backorder).workorder_ids:
if wo.state in ('done', 'cancel'):
continue
wo.current_quality_check_id.update(wo._defaults_from_move(wo.move_id))
if wo.move_id:
wo.current_quality_check_id._update_component_quantity()
if not self.env.context.get('no_start_next'):
if self.operation_id:
return backorder.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_id).open_tablet_view()
else:
index = list(self.production_id.workorder_ids).index(self)
return backorder.workorder_ids[index].open_tablet_view()
return True
def _defaults_from_move(self, move):
self.ensure_one()
vals = {'move_id': move.id}
move_line_id = move.move_line_ids.filtered(lambda sml: sml._without_quality_checks())[:1]
if move_line_id:
vals.update({
'move_line_id': move_line_id.id,
'lot_id': move_line_id.lot_id.id,
'qty_done': move_line_id.reserved_uom_qty or 1.0
})
return vals
# --------------------------
# Buttons from quality.check
# --------------------------
def open_tablet_view(self):
self.ensure_one()
if not self.is_user_working and self.working_state != 'blocked' and self.state in ('ready', 'waiting', 'progress', 'pending'):
self.button_start()
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.tablet_client_action")
action['target'] = 'fullscreen'
action['res_id'] = self.id
action['context'] = {
'active_id': self.id,
'from_production_order': self.env.context.get('from_production_order'),
'from_manufacturing_order': self.env.context.get('from_manufacturing_order')
}
return action
def action_open_manufacturing_order(self):
action = self.with_context(no_start_next=True).do_finish()
try:
with self.env.cr.savepoint():
res = self.production_id.button_mark_done()
if res is not True:
res['context'] = dict(res['context'], from_workorder=True)
return res
except (UserError, ValidationError) as e:
# log next activity on MO with error message
self.production_id.activity_schedule(
'mail.mail_activity_data_warning',
note=e.name,
summary=('The %s could not be closed') % (self.production_id.name),
user_id=self.env.user.id)
return {
'type': 'ir.actions.act_window',
'res_model': 'mrp.production',
'views': [[self.env.ref('mrp.mrp_production_form_view').id, 'form']],
'res_id': self.production_id.id,
'target': 'main',
}
return action
def do_finish(self):
action = True
if self.state != 'done':
action = self.record_production()
if action is not True:
return action
# workorder tree view action should redirect to the same view instead of workorder kanban view when WO mark as done.
return self.action_back()
def get_workorder_data(self):
# order quality check chain
ele = self.check_ids.filtered(lambda check: not check.previous_check_id)
sorted_check_list = []
while ele:
sorted_check_list += ele.ids
ele = ele.next_check_id
data = {
'mrp.workorder': self.read(self._get_fields_for_tablet(), load=False)[0],
'quality.check': self.check_ids._get_fields_for_tablet(sorted_check_list),
'operation': self.operation_id.read(self.operation_id._get_fields_for_tablet())[0] if self.operation_id else {},
'working_state': self.workcenter_id.working_state,
'views': {
'workorder': self.env.ref('mrp_workorder.mrp_workorder_view_form_tablet').id,
'check': self.env.ref('mrp_workorder.quality_check_view_form_tablet').id,
},
}
return data
def get_summary_data(self):
self.ensure_one()
# show rainbow man only the first time
show_rainbow = any(not t.date_end for t in self.time_ids)
self.end_all()
if any(step.quality_state == 'none' for step in self.check_ids):
raise UserError(_('You still need to do the quality checks!'))
last30op = self.env['mrp.workorder'].search_read([
('operation_id', '=', self.operation_id.id),
('date_finished', '>', fields.datetime.today() - relativedelta(days=30)),
], ['duration'], order='duration')
last30op = [item['duration'] for item in last30op]
passed_checks = len(list(check.quality_state == 'pass' for check in self.check_ids))
if passed_checks:
score = int(3.0 * len(self.check_ids) / passed_checks)
elif not self.check_ids:
score = 3
else:
score = 0
return {
'duration': self.duration,
'position': bisect_left(last30op, self.duration), # which position regarded other workorders ranked by duration
'quality_score': score,
'show_rainbow': show_rainbow,
}
def _action_confirm(self):
res = super()._action_confirm()
self.filtered(lambda wo: not wo.check_ids)._create_checks()
return res
def _update_qty_producing(self, quantity):
if float_is_zero(quantity, precision_rounding=self.product_uom_id.rounding):
self.check_ids.unlink()
super()._update_qty_producing(quantity)
def _web_gantt_progress_bar_workcenter_id(self, res_ids, start, stop):
self.env['mrp.workorder'].check_access_rights('read')
workcenters = self.env['mrp.workcenter'].search([('id', 'in', res_ids)])
workorders = self.env['mrp.workorder'].search([
('workcenter_id', 'in', res_ids),
('state', 'not in', ['done', 'cancel']),
('date_planned_start', '<=', stop.replace(tzinfo=None)),
('date_planned_finished', '>=', start.replace(tzinfo=None)),
])
planned_hours = defaultdict(float)
workcenters_work_intervals, dummy = workcenters.resource_id._get_valid_work_intervals(start, stop)
for workorder in workorders:
max_start = max(start, utc.localize(workorder.date_planned_start))
min_end = min(stop, utc.localize(workorder.date_planned_finished))
interval = Intervals([(max_start, min_end, self.env['resource.calendar.attendance'])])
work_intervals = interval & workcenters_work_intervals[workorder.workcenter_id.resource_id.id]
planned_hours[workorder.workcenter_id] += sum_intervals(work_intervals)
work_hours = {
id: sum_intervals(work_intervals) for id, work_intervals in workcenters_work_intervals.items()
}
return {
workcenter.id: {
'value': planned_hours[workcenter],
'max_value': work_hours.get(workcenter.resource_id.id, 0.0),
}
for workcenter in workcenters
}
def _web_gantt_progress_bar(self, field, res_ids, start, stop):
if field == 'workcenter_id':
return dict(
self._web_gantt_progress_bar_workcenter_id(res_ids, start, stop),
warning=_("This workcenter isn't expected to have open workorders during this period. Work hours :"),
)
raise NotImplementedError("This Progress Bar is not implemented.")
@api.model
def gantt_progress_bar(self, fields, res_ids, date_start_str, date_stop_str):
start_utc, stop_utc = string_to_datetime(date_start_str), string_to_datetime(date_stop_str)
today = datetime.now(utc).replace(hour=0, minute=0, second=0, microsecond=0)
start_utc = max(start_utc, today)
progress_bars = {}
for field in fields:
progress_bars[field] = self._web_gantt_progress_bar(field, res_ids[field], start_utc, stop_utc)
return progress_bars
def _get_fields_for_tablet(self):
""" List of fields on the workorder object that are needed by the tablet
client action. The purpose of this function is to be overridden in order
to inject new fields to the client action.
"""
return [
'production_id',
'name',
'qty_producing',
'state',
'company_id',
'workcenter_id',
'current_quality_check_id',
'operation_note',
]

View File

@@ -0,0 +1,543 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from markupsafe import Markup
from odoo import SUPERUSER_ID, api, fields, models, _
from odoo.exceptions import UserError
from odoo.fields import Command
from odoo.tools import float_compare, float_round
class TestType(models.Model):
_inherit = "quality.point.test_type"
allow_registration = fields.Boolean(search='_get_domain_from_allow_registration',
store=False, default=False)
def _get_domain_from_allow_registration(self, operator, value):
if value:
return []
else:
return [('technical_name', 'not in', ['register_byproducts', 'register_consumed_materials', 'print_label'])]
class MrpRouting(models.Model):
_inherit = "mrp.routing.workcenter"
quality_point_ids = fields.One2many('quality.point', 'operation_id', copy=True)
quality_point_count = fields.Integer('Instructions', compute='_compute_quality_point_count')
@api.depends('quality_point_ids')
def _compute_quality_point_count(self):
read_group_res = self.env['quality.point'].sudo().read_group(
[('id', 'in', self.quality_point_ids.ids)],
['operation_id'], 'operation_id'
)
data = dict((res['operation_id'][0], res['operation_id_count']) for res in read_group_res)
for operation in self:
operation.quality_point_count = data.get(operation.id, 0)
def write(self, vals):
res = super().write(vals)
if 'bom_id' in vals:
self.quality_point_ids._change_product_ids_for_bom(self.bom_id)
return res
def copy(self, default=None):
res = super().copy(default)
if default and "bom_id" in default:
res.quality_point_ids._change_product_ids_for_bom(res.bom_id)
return res
def toggle_active(self):
self.with_context(active_test=False).quality_point_ids.toggle_active()
return super().toggle_active()
def action_mrp_workorder_show_steps(self):
self.ensure_one()
if self.bom_id.picking_type_id:
picking_type_ids = self.bom_id.picking_type_id.ids
else:
picking_type_ids = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')], limit=1).ids
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.action_mrp_workorder_show_steps")
ctx = {
'default_company_id': self.company_id.id,
'default_operation_id': self.id,
'default_picking_type_ids': picking_type_ids,
}
action.update({'context': ctx, 'domain': [('operation_id', '=', self.id)]})
return action
def _get_fields_for_tablet(self):
""" List of fields on the operation object that are needed by the tablet
client action. The purpose of this function is to be overridden in order
to inject new fields to the client action.
"""
return [
'worksheet',
'id',
]
class QualityPoint(models.Model):
_inherit = "quality.point"
def _default_product_ids(self):
# Determines a default product from the default operation's BOM.
operation_id = self.env.context.get('default_operation_id')
if operation_id:
bom = self.env['mrp.routing.workcenter'].browse(operation_id).bom_id
return bom.product_id.ids if bom.product_id else bom.product_tmpl_id.product_variant_id.ids
is_workorder_step = fields.Boolean(compute='_compute_is_workorder_step')
operation_id = fields.Many2one(
'mrp.routing.workcenter', 'Step', check_company=True)
bom_id = fields.Many2one(related='operation_id.bom_id')
bom_active = fields.Boolean('Related Bill of Material Active', related='bom_id.active')
component_ids = fields.One2many('product.product', compute='_compute_component_ids')
product_ids = fields.Many2many(
default=_default_product_ids,
domain="operation_id and [('id', 'in', bom_product_ids)] or [('type', 'in', ('product', 'consu')), '|', ('company_id', '=', False), ('company_id', '=', company_id)]")
bom_product_ids = fields.One2many('product.product', compute="_compute_bom_product_ids")
test_type_id = fields.Many2one(
'quality.point.test_type',
domain="[('allow_registration', '=', operation_id and is_workorder_step)]")
test_report_type = fields.Selection([('pdf', 'PDF'), ('zpl', 'ZPL')], string="Report Type", default="pdf", required=True)
source_document = fields.Selection(
selection=[('operation', 'Specific Page of Operation Worksheet'), ('step', 'Custom')],
string="Step Document",
default='operation')
worksheet_page = fields.Integer('Worksheet Page', default=1)
worksheet_document = fields.Binary('Image/PDF')
worksheet_url = fields.Char('Google doc URL')
# Used with type register_consumed_materials the product raw to encode.
component_id = fields.Many2one('product.product', 'Product To Register', check_company=True)
@api.onchange('bom_product_ids', 'is_workorder_step')
def _onchange_bom_product_ids(self):
if self.is_workorder_step and self.bom_product_ids:
self.product_ids = self.product_ids._origin & self.bom_product_ids
self.product_category_ids = False
@api.depends('bom_id.product_id', 'bom_id.product_tmpl_id.product_variant_ids', 'is_workorder_step', 'bom_id')
def _compute_bom_product_ids(self):
self.bom_product_ids = False
points_for_workorder_step = self.filtered(lambda p: p.operation_id and p.bom_id)
for point in points_for_workorder_step:
bom_product_ids = point.bom_id.product_id or point.bom_id.product_tmpl_id.product_variant_ids
point.bom_product_ids = bom_product_ids.filtered(lambda p: not p.company_id or p.company_id == point.company_id._origin)
@api.depends('product_ids', 'test_type_id', 'is_workorder_step')
def _compute_component_ids(self):
self.component_ids = False
for point in self:
if not point.is_workorder_step or not self.bom_id or point.test_type not in ('register_consumed_materials', 'register_byproducts'):
point.component_id = None
continue
if point.test_type == 'register_byproducts':
point.component_ids = point.bom_id.byproduct_ids.product_id
else:
bom_products = point.bom_id.product_id or point.bom_id.product_tmpl_id.product_variant_ids
# If product_ids is set the step will exist only for these product variant then we can filter out for the bom explode
if point.product_ids:
bom_products &= point.product_ids._origin
component_product_ids = set()
for product in bom_products:
dummy, lines_done = point.bom_id.explode(product, 1.0)
component_product_ids |= {line[0].product_id.id for line in lines_done}
point.component_ids = self.env['product.product'].browse(component_product_ids)
@api.depends('operation_id', 'picking_type_ids')
def _compute_is_workorder_step(self):
for quality_point in self:
quality_point.is_workorder_step = quality_point.picking_type_ids and\
all(pt.code == 'mrp_operation' for pt in quality_point.picking_type_ids)
def _change_product_ids_for_bom(self, bom_id):
products = bom_id.product_id or bom_id.product_tmpl_id.product_variant_ids
self.product_ids = [Command.set(products.ids)]
def _get_comparison_values(self):
if not self:
return False
self.ensure_one()
return tuple(self[key] for key in ('test_type_id', 'title', 'component_id', 'sequence'))
@api.onchange('operation_id')
def _onchange_operation_id(self):
if self.operation_id:
self._change_product_ids_for_bom(self.bom_id)
class QualityAlert(models.Model):
_inherit = "quality.alert"
workorder_id = fields.Many2one('mrp.workorder', 'Operation', check_company=True)
workcenter_id = fields.Many2one('mrp.workcenter', 'Work Center', check_company=True)
production_id = fields.Many2one('mrp.production', "Production Order", check_company=True)
class QualityCheck(models.Model):
_inherit = "quality.check"
workorder_id = fields.Many2one(
'mrp.workorder', 'Operation', check_company=True)
workcenter_id = fields.Many2one('mrp.workcenter', related='workorder_id.workcenter_id', store=True, readonly=True) # TDE: necessary ?
production_id = fields.Many2one(
'mrp.production', 'Production Order', check_company=True)
# doubly linked chain for tablet view navigation
next_check_id = fields.Many2one('quality.check')
previous_check_id = fields.Many2one('quality.check')
# For components registration
move_id = fields.Many2one(
'stock.move', 'Stock Move', check_company=True)
move_line_id = fields.Many2one(
'stock.move.line', 'Stock Move Line', check_company=True)
component_id = fields.Many2one(
'product.product', 'Component', check_company=True)
component_uom_id = fields.Many2one('uom.uom', related='move_id.product_uom', readonly=True)
qty_done = fields.Float('Done', digits='Product Unit of Measure')
finished_lot_id = fields.Many2one('stock.lot', 'Finished Lot/Serial', related='production_id.lot_producing_id', store=True)
additional = fields.Boolean('Register additional product', compute='_compute_additional')
component_tracking = fields.Selection(related='component_id.tracking', string="Is Component Tracked")
# Workorder specific fields
component_remaining_qty = fields.Float('Remaining Quantity for Component', compute='_compute_component_data', digits='Product Unit of Measure')
component_qty_to_do = fields.Float(compute='_compute_component_qty_to_do')
is_user_working = fields.Boolean(related="workorder_id.is_user_working")
consumption = fields.Selection(related="workorder_id.consumption")
working_state = fields.Selection(related="workorder_id.working_state")
is_deleted = fields.Boolean('Deleted in production')
# Computed fields
title = fields.Char('Title', compute='_compute_title')
result = fields.Char('Result', compute='_compute_result')
# Used to group the steps belonging to the same production
# We use a float because it is actually filled in by the produced quantity at the step creation.
finished_product_sequence = fields.Float('Finished Product Sequence Number')
worksheet_document = fields.Binary('Image/PDF')
worksheet_page = fields.Integer(related='point_id.worksheet_page')
@api.model_create_multi
def create(self, values):
points = self.env['quality.point'].search([
('id', 'in', [value.get('point_id') for value in values]),
('component_id', '!=', False)
])
for value in values:
if not value.get('component_id') and value.get('point_id'):
point = points.filtered(lambda p: p.id == value.get('point_id'))
if point:
value['component_id'] = point.component_id.id
return super(QualityCheck, self).create(values)
@api.depends('test_type_id', 'component_id', 'component_id.name', 'workorder_id', 'workorder_id.name')
def _compute_title(self):
super()._compute_title()
for check in self:
if not check.point_id or check.component_id:
check.title = '{} "{}"'.format(check.test_type_id.display_name, check.component_id.name or check.workorder_id.name)
@api.depends('point_id', 'quality_state', 'component_id', 'component_uom_id', 'lot_id', 'qty_done')
def _compute_result(self):
for check in self:
if check.quality_state == 'none':
check.result = ''
else:
check.result = check._get_check_result()
@api.depends('move_id')
def _compute_additional(self):
""" The stock_move is linked to additional workorder line only at
record_production. So line without move during production are additionnal
ones. """
for check in self:
check.additional = not check.move_id
@api.depends('qty_done', 'component_remaining_qty')
def _compute_component_qty_to_do(self):
for wo in self:
wo.component_qty_to_do = wo.qty_done - wo.component_remaining_qty
def _get_check_result(self):
if self.test_type in ('register_consumed_materials', 'register_byproducts') and self.lot_id:
return '{} - {}, {} {}'.format(self.component_id.name, self.lot_id.name, self.qty_done, self.component_uom_id.name)
elif self.test_type in ('register_consumed_materials', 'register_byproducts'):
return '{}, {} {}'.format(self.component_id.name, self.qty_done, self.component_uom_id.name)
else:
return ''
@api.depends('workorder_id.state', 'quality_state', 'workorder_id.qty_producing',
'component_tracking', 'test_type', 'component_id', 'move_line_id.lot_id'
)
def _compute_component_data(self):
self.component_remaining_qty = False
self.component_uom_id = False
for check in self:
if check.test_type in ('register_byproducts', 'register_consumed_materials'):
if check.quality_state == 'none':
completed_lines = check.workorder_id.move_line_ids.filtered(lambda l: l.lot_id) if check.component_id.tracking != 'none' else check.workorder_id.move_line_ids
if check.move_id.additional:
qty = check.workorder_id.qty_remaining
else:
qty = check.workorder_id.qty_producing
check.component_remaining_qty = self._prepare_component_quantity(check.move_id, qty) - sum(completed_lines.mapped('qty_done'))
check.component_uom_id = check.move_id.product_uom
def action_print(self):
if self.product_id.uom_id.category_id == self.env.ref('uom.product_uom_categ_unit'):
qty = int(self.workorder_id.qty_producing)
else:
qty = 1
quality_point_id = self.point_id
report_type = quality_point_id.test_report_type
if self.product_id.tracking == 'none':
xml_id = 'product.action_open_label_layout'
wizard_action = self.env['ir.actions.act_window']._for_xml_id(xml_id)
wizard_action['context'] = {'default_product_ids': self.product_id.ids}
if report_type == 'zpl':
wizard_action['context']['default_print_format'] = 'zpl'
res = wizard_action
else:
if self.workorder_id.finished_lot_id:
if report_type == 'zpl':
xml_id = 'stock.label_lot_template'
else:
xml_id = 'stock.action_report_lot_label'
res = self.env.ref(xml_id).report_action([self.workorder_id.finished_lot_id.id] * qty)
else:
raise UserError(_('You did not set a lot/serial number for '
'the final product'))
res['id'] = self.env.ref(xml_id).id
# The button goes immediately to the next step
self._next()
return res
def action_next(self):
self.ensure_one()
return self._next()
def action_continue(self):
self.ensure_one()
self._next(continue_production=True)
def add_check_in_chain(self, activity=True):
self.ensure_one()
if self.workorder_id.current_quality_check_id:
self._insert_in_chain('after', self.workorder_id.current_quality_check_id)
else:
self.workorder_id.current_quality_check_id = self
if self.workorder_id.production_id.bom_id and activity:
body = Markup(_("<b>New Step suggested by %s</b><br/>"
"<b>Reason:</b>"
"%s", self.env.user.name, self.additional_note
))
self.env['mail.activity'].sudo().create({
'res_model_id': self.env.ref('mrp.model_mrp_bom').id,
'res_id': self.workorder_id.production_id.bom_id.id,
'user_id': self.workorder_id.product_id.responsible_id.id or SUPERUSER_ID,
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
'summary': _('BoM feedback %s (%s)', self.title, self.workorder_id.production_id.name),
'note': body,
})
@api.model
def _prepare_component_quantity(self, move, qty_producing):
""" helper that computes quantity to consume (or to create in case of byproduct)
depending on the quantity producing and the move's unit factor"""
if move.product_id.tracking == 'serial':
uom = move.product_id.uom_id
else:
uom = move.product_uom
return move.product_uom._compute_quantity(
qty_producing * move.unit_factor,
uom,
round=False
)
def _create_extra_move_lines(self):
"""Create new sml if quantity produced is bigger than the reserved one"""
vals_list = []
# apply putaway
location_dest_id = self.move_id.location_dest_id._get_putaway_strategy(self.move_id.product_id)
quants = self.env['stock.quant']._gather(self.product_id, self.move_id.location_id, lot_id=self.lot_id, strict=False)
# Search for a sub-locations where the product is available.
# Loop on the quants to get the locations. If there is not enough
# quantity into stock, we take the move location. Anyway, no
# reservation is made, so it is still possible to change it afterwards.
shared_vals = {
'move_id': self.move_id.id,
'product_id': self.move_id.product_id.id,
'location_dest_id': location_dest_id.id,
'reserved_uom_qty': 0,
'product_uom_id': self.move_id.product_uom.id,
'lot_id': self.lot_id.id,
'company_id': self.move_id.company_id.id,
}
for quant in quants:
vals = shared_vals.copy()
quantity = quant.quantity - quant.reserved_quantity
quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom_id, rounding_method='HALF-UP')
rounding = quant.product_uom_id.rounding
if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or
float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0):
continue
vals.update({
'location_id': quant.location_id.id,
'qty_done': min(quantity, self.qty_done),
})
vals_list.append(vals)
self.qty_done -= vals['qty_done']
# If all the qty_done is distributed, we can close the loop
if float_compare(self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) <= 0:
break
if float_compare(self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) > 0:
vals = shared_vals.copy()
vals.update({
'location_id': self.move_id.location_id.id,
'qty_done': self.qty_done,
})
vals_list.append(vals)
return vals_list
def _next(self, continue_production=False):
""" This function:
- first: fullfill related move line with right lot and validated quantity.
- second: Generate new quality check for remaining quantity and link them to the original check.
- third: Pass to the next check or return a failure message.
"""
self.ensure_one()
rounding = self.workorder_id.product_uom_id.rounding
if float_compare(self.workorder_id.qty_producing, 0, precision_rounding=rounding) <= 0:
raise UserError(_('Please ensure the quantity to produce is greater than 0.'))
elif self.test_type in ('register_byproducts', 'register_consumed_materials'):
# Form validation
# in case we use continue production instead of validate button.
# We would like to consume 0 and leave lot_id blank to close the consumption
rounding = self.component_uom_id.rounding
if self.component_tracking != 'none' and not self.lot_id and self.qty_done != 0:
raise UserError(_('Please enter a Lot/SN.'))
if float_compare(self.qty_done, 0, precision_rounding=rounding) < 0:
raise UserError(_('Please enter a positive quantity.'))
# Write the lot and qty to the move line
if self.move_line_id:
rounding = self.move_line_id.product_uom_id.rounding
if float_compare(self.qty_done, self.move_line_id.reserved_uom_qty, precision_rounding=rounding) >= 0:
self.move_line_id.write({
'qty_done': self.qty_done,
'lot_id': self.lot_id.id,
})
else:
new_qty_reserved = self.move_line_id.reserved_uom_qty - self.qty_done
default = {
'reserved_uom_qty': new_qty_reserved,
'qty_done': 0,
}
self.move_line_id.copy(default=default)
self.move_line_id.with_context(bypass_reservation_update=True).write({
'reserved_uom_qty': self.qty_done,
'qty_done': self.qty_done,
})
self.move_line_id.lot_id = self.lot_id
else:
line = self.env['stock.move.line'].create(self._create_extra_move_lines())
self.move_line_id = line[:1]
if continue_production:
self.workorder_id._create_subsequent_checks()
if self.test_type == 'picture' and not self.picture:
raise UserError(_('Please upload a picture.'))
if self.quality_state == 'none':
self.do_pass()
self.workorder_id._change_quality_check(position='next')
def _update_component_quantity(self):
if self.component_tracking == 'serial':
self._origin.qty_done = self.component_id.uom_id._compute_quantity(1, self.component_uom_id, rounding_method='HALF-UP')
return
move = self.move_id
# Compute the new quantity for the current component
rounding = move.product_uom.rounding
new_qty = self._prepare_component_quantity(move, self.workorder_id.qty_producing)
qty_todo = float_round(new_qty, precision_rounding=rounding)
qty_todo = qty_todo - move.quantity_done
if self.move_line_id and self.move_line_id.lot_id:
qty_todo = min(self.move_line_id.reserved_uom_qty, qty_todo)
self.qty_done = qty_todo
def _insert_in_chain(self, position, relative):
"""Insert the quality check `self` in a chain of quality checks.
The chain of quality checks is implicitly given by the `relative` argument,
i.e. by following its `previous_check_id` and `next_check_id` fields.
:param position: Where we need to insert `self` according to `relative`
:type position: string
:param relative: Where we need to insert `self` in the chain
:type relative: A `quality.check` record.
"""
self.ensure_one()
assert position in ['before', 'after']
if position == 'before':
new_previous = relative.previous_check_id
self.next_check_id = relative
self.previous_check_id = new_previous
new_previous.next_check_id = self
relative.previous_check_id = self
else:
new_next = relative.next_check_id
self.next_check_id = new_next
self.previous_check_id = relative
new_next.previous_check_id = self
relative.next_check_id = self
@api.model
def _get_fields_list_for_tablet(self):
return [
'lot_id',
'move_id',
'move_line_id',
'note',
'additional_note',
'title',
'quality_state',
'qty_done',
'test_type_id',
'test_type',
'user_id',
'picture',
'additional',
'worksheet_document',
'worksheet_page',
'is_deleted',
'point_id',
]
def _get_fields_for_tablet(self, sorted_check_list):
""" List of fields on the quality check object that are needed by the tablet
client action. The purpose of this function is to be overridden in order
to inject new fields to the client action.
"""
if sorted_check_list:
self = self.browse(sorted_check_list)
values = self.read(self._get_fields_list_for_tablet(), load=False)
for check in values:
check['worksheet_url'] = self.env['quality.check'].browse(check['id']).point_id.worksheet_url
return values

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 ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
group_mrp_wo_tablet_timer = fields.Boolean("Timer", implied_group="mrp_workorder.group_mrp_wo_tablet_timer")
def set_values(self):
super().set_values()
if not self.user_has_groups('mrp.group_mrp_manager'):
return
register_byproducts = self.env.ref('mrp_workorder.test_type_register_byproducts').sudo()
if register_byproducts.active != self.group_mrp_byproducts:
register_byproducts.active = self.group_mrp_byproducts

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class StockMove(models.Model):
_inherit = 'stock.move'
def _should_bypass_set_qty_producing(self):
production = self.raw_material_production_id or self.production_id
if production and (self.has_tracking == 'none' or production.use_auto_consume_components_lots) and ((self.product_id in production.workorder_ids.quality_point_ids.component_id) or self.operation_id):
return True
return super()._should_bypass_set_qty_producing()
def _action_assign(self, force_qty=False):
res = super()._action_assign(force_qty=force_qty)
for workorder in self.raw_material_production_id.workorder_ids:
for check in workorder.check_ids:
if check.test_type not in ('register_consumed_materials', 'register_byproducts'):
continue
if check.move_line_id:
continue
check.write(workorder._defaults_from_move(check.move_id))
return res

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class StockMoveLine(models.Model):
_inherit = 'stock.move.line'
quality_check_ids = fields.One2many('quality.check', 'move_line_id', string='Check')
def _without_quality_checks(self):
self.ensure_one()
return not self.quality_check_ids