diff --git a/jikimo_frontend/static/src/scss/custom_style.scss b/jikimo_frontend/static/src/scss/custom_style.scss index b70cc749..7f11b5f5 100644 --- a/jikimo_frontend/static/src/scss/custom_style.scss +++ b/jikimo_frontend/static/src/scss/custom_style.scss @@ -503,4 +503,27 @@ div:has(.o_required_modifier) > label::before { color: #fff; background-color: #4A4F59; border-color: #4A4F59; +} + +// 功能刀具组装单 弹窗样式 +.o_horizontal_separator.mt-4.mb-3.text-uppercase.fw-bolder.small ~ div.col-lg-6 .o_inner_group.col-lg-6 { + width: 100%; +} +.o_horizontal_separator.mt-4.mb-3.text-uppercase.fw-bolder.small ~ div .o_inner_group .o_wrap_field.d-flex.d-sm-contents.flex-column{ + display: flex!important; + flex-direction: row!important; + input { + border-bottom: 1px solid; + } +} + +// 设置表格横向滚动 +.o_list_renderer.o_renderer { + max-width: 100%; + overflow-x: auto; +} + +// 设置表单页面label文本不换行 +.o_form_view .o_group .o_wrap_label .o_form_label { + white-space: nowrap; } \ No newline at end of file diff --git a/mrp_workorder/models/mrp_workorder.py b/mrp_workorder/models/mrp_workorder.py index 81be5f2d..01363bb5 100644 --- a/mrp_workorder/models/mrp_workorder.py +++ b/mrp_workorder/models/mrp_workorder.py @@ -27,7 +27,7 @@ class MrpWorkcenter(models.Model): class MrpProductionWorkcenterLine(models.Model): _name = 'mrp.workorder' - _inherit = ['mrp.workorder', 'barcodes.barcode_events_mixin'] + _inherit = ['mrp.workorder', 'barcodes.barcode_events_mixin', 'mail.thread', 'mail.activity.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') @@ -47,14 +47,17 @@ class MrpProductionWorkcenterLine(models.Model): 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) + 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) + 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_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') @@ -65,7 +68,8 @@ class MrpProductionWorkcenterLine(models.Model): 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) + 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') @@ -91,7 +95,8 @@ class MrpProductionWorkcenterLine(models.Model): @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) + 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) @@ -138,7 +143,8 @@ class MrpProductionWorkcenterLine(models.Model): 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'), + '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): @@ -152,7 +158,7 @@ class MrpProductionWorkcenterLine(models.Model): """ # 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\ + 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 @@ -279,7 +285,8 @@ class MrpProductionWorkcenterLine(models.Model): 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) + 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', @@ -320,7 +327,8 @@ class MrpProductionWorkcenterLine(models.Model): 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) + 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 @@ -342,11 +350,13 @@ class MrpProductionWorkcenterLine(models.Model): 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) + 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) + 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 @@ -363,8 +373,10 @@ class MrpProductionWorkcenterLine(models.Model): 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 + 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, @@ -412,7 +424,8 @@ class MrpProductionWorkcenterLine(models.Model): 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: + 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': @@ -423,7 +436,8 @@ class MrpProductionWorkcenterLine(models.Model): 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') + 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) @@ -442,7 +456,8 @@ class MrpProductionWorkcenterLine(models.Model): 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() + 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() @@ -466,7 +481,8 @@ class MrpProductionWorkcenterLine(models.Model): 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'): + 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' @@ -521,7 +537,8 @@ class MrpProductionWorkcenterLine(models.Model): 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 {}, + '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, @@ -553,7 +570,8 @@ class MrpProductionWorkcenterLine(models.Model): return { 'duration': self.duration, - 'position': bisect_left(last30op, self.duration), # which position regarded other workorders ranked by duration + 'position': bisect_left(last30op, self.duration), + # which position regarded other workorders ranked by duration 'quality_score': score, 'show_rainbow': show_rainbow, } diff --git a/mrp_workorder/views/mrp_workorder_views.xml b/mrp_workorder/views/mrp_workorder_views.xml index 37702c00..8c208f19 100644 --- a/mrp_workorder/views/mrp_workorder_views.xml +++ b/mrp_workorder/views/mrp_workorder_views.xml @@ -27,6 +27,13 @@ + +
+ + + +
+
diff --git a/sf_base/static/src/scss/format_img.scss b/sf_base/static/src/scss/format_img.scss index 279c5fff..982d3c50 100644 --- a/sf_base/static/src/scss/format_img.scss +++ b/sf_base/static/src/scss/format_img.scss @@ -1,7 +1,7 @@ .o_data_row .w-100 { width: 40px !important; height: 40px !important; - display: block !important; + //display: block !important; } .o_list_renderer .o_list_table tbody > tr > td:not(.o_list_record_selector):not(.o_handle_cell):not(.o_list_button):not(.o_list_record_remove) { diff --git a/sf_bf_connect/models/jd_eclp.py b/sf_bf_connect/models/jd_eclp.py index 6d6a0e38..e381242a 100644 --- a/sf_bf_connect/models/jd_eclp.py +++ b/sf_bf_connect/models/jd_eclp.py @@ -67,9 +67,10 @@ class JdEclp(models.Model): """ 判断是否为出库单 """ - if self.name: - is_check_out = self.name.split('/') - self.check_out = is_check_out[1] + for record in self: + if record.name: + is_check_out = record.name.split('/') + record.check_out = is_check_out[1] @api.depends('carrier_tracking_ref') def query_bill_pdf(self): diff --git a/sf_dlm_management/views/product_template_management_view.xml b/sf_dlm_management/views/product_template_management_view.xml index dea0150d..accc23a0 100644 --- a/sf_dlm_management/views/product_template_management_view.xml +++ b/sf_dlm_management/views/product_template_management_view.xml @@ -10,6 +10,7 @@ + + @@ -47,10 +50,10 @@ - - @@ -95,6 +98,16 @@ + + {'readonly': ['|',('id','!=',False),('categ_type', '=', + '刀具')], 'required': True} + + + + + + + @@ -455,7 +468,8 @@ - + diff --git a/sf_manufacturing/controllers/controllers.py b/sf_manufacturing/controllers/controllers.py index b9027a9f..ddbf4048 100644 --- a/sf_manufacturing/controllers/controllers.py +++ b/sf_manufacturing/controllers/controllers.py @@ -215,6 +215,7 @@ class Manufacturing_Connect(http.Controller): if workorder.state != 'progress': res = {'Succeed': False, 'ErrorCode': 202, 'Error': '该工单未开始'} return json.JSONEncoder().encode(res) + # workorder.write({'date_finished': datetime.now()}) workorder.button_finish() # workorder.process_state = '待解除装夹' # workorder.sudo().production_id.process_state = '待解除装夹' diff --git a/sf_manufacturing/models/agv_setting.py b/sf_manufacturing/models/agv_setting.py index 64af1d90..06f9edba 100644 --- a/sf_manufacturing/models/agv_setting.py +++ b/sf_manufacturing/models/agv_setting.py @@ -2,7 +2,8 @@ import requests import logging import time -from odoo import fields, models +from odoo import fields, models, api +from odoo.exceptions import UserError class AgvSetting(models.Model): @@ -59,11 +60,17 @@ class AgvTaskRoute(models.Model): ('F01', '搬运'), ], string='任务类型', default="F01") route_type = fields.Selection([ ('上产线', '上产线'), ('下产线', '下产线'), ('运送空料架', '运送空料架')], string='类型') - start_site_id = fields.Many2one('sf.agv.site', '起点接驳站位置编号') - end_site_id = fields.Many2one('sf.agv.site', '终点接驳站位置编号') + start_site_id = fields.Many2one('sf.agv.site', '起点接驳站') + end_site_id = fields.Many2one('sf.agv.site', '终点接驳站') destination_production_line_id = fields.Many2one('sf.production.line', '目的生产线') active = fields.Boolean('有效', default=True) + @api.constrains('end_site_id') + def _check_end_site_id(self): + if self.end_site_id: + if self.end_site_id == self.start_site_id: + raise UserError("您选择的终点接驳站与起点接驳站重复,请重新选择") + class Center_controlInterfaceLog(models.Model): _name = 'center_control.interface.log' diff --git a/sf_manufacturing/models/mrp_production.py b/sf_manufacturing/models/mrp_production.py index 34e00aee..0f428d43 100644 --- a/sf_manufacturing/models/mrp_production.py +++ b/sf_manufacturing/models/mrp_production.py @@ -5,7 +5,7 @@ import re import requests from itertools import groupby from odoo import api, fields, models, _ -from odoo.exceptions import UserError,ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.addons.sf_base.commons.common import Common from odoo.tools import float_compare, float_round, float_is_zero, format_datetime @@ -53,13 +53,13 @@ class MrpProduction(models.Model): active = fields.Boolean(string='已归档', default=True) programming_no = fields.Char('编程单号') work_state = fields.Char('业务状态') - programming_state = fields.Char('编程状态') + programming_state = fields.Char('编程状态', tracking=True) glb_file = fields.Binary("glb模型文件") - production_line_id = fields.Many2one('sf.production.line', string='生产线') + production_line_id = fields.Many2one('sf.production.line', string='生产线', tracking=True) plan_start_processing_time = fields.Datetime('计划开始加工时间') production_line_state = fields.Selection( [('待上产线', '待上产线'), ('已上产线', '已上产线'), ('已下产线', '已下产线')], - string='上/下产线', default='待上产线') + string='上/下产线', default='待上产线', tracking=True) # 工序状态 # Todo 研究下用法 process_state = fields.Selection([ @@ -77,6 +77,7 @@ class MrpProduction(models.Model): part_drawing = fields.Binary('零件图纸') manual_quotation = fields.Boolean('人工编程', default=False, readonly=True) + rework_production = fields.Many2one('mrp.production', string='返工的制造订单') @api.depends( 'move_raw_ids.state', 'move_raw_ids.quantity_done', 'move_finished_ids.state', @@ -154,8 +155,30 @@ class MrpProduction(models.Model): for production in self: production.maintenance_count = len(production.request_ids) + # 制造订单报废:编程单更新 + def updateCNC(self): + try: + res = {'production_no': self.name, 'programming_no': self.programming_no, + 'order_no': self.origin} + logging.info('res=%s:' % res) + configsettings = self.env['res.config.settings'].get_values() + config_header = Common.get_headers(self, configsettings['token'], configsettings['sf_secret_key']) + url = '/api/intelligent_programming/update_intelligent_programmings' + config_url = configsettings['sf_url'] + url + res['token'] = configsettings['token'] + ret = requests.post(config_url, json={}, data=res, headers=config_header) + ret = ret.json() + logging.info('updateCNC-ret:%s' % ret) + if ret['status'] == 1: + self.write({'work_state': '已编程'}) + else: + raise UserError(ret['message']) + except Exception as e: + logging.info('updateCNC error:%s' % e) + raise UserError("更新程单失败,请联系管理员") + # cnc程序获取 - def fetchCNC(self, production_names): + def fetchCNC(self, production_names, scrap_production): cnc = self.env['mrp.production'].search([('id', '=', self.id)]) quick_order = self.env['quick.easy.order'].search( [('name', '=', cnc.product_id.default_code.rsplit('-', 1)[0])]) @@ -171,6 +194,8 @@ class MrpProduction(models.Model): 'production_no': production_names, 'machine_tool_code': '', 'product_name': cnc.product_id.name, + 'remanufacture_type': '' if not scrap_production else scrap_production.workorder_ids.filtered( + lambda b: b.routing_type == "CNC加工").test_results, 'model_code': cnc.product_id.model_code, 'material_code': self.env['sf.production.materials'].search( [('id', '=', cnc.product_id.materials_id.id)]).materials_no, @@ -312,10 +337,12 @@ class MrpProduction(models.Model): [('product_id.id', '=', production.product_id.id), ('origin', '=', production.origin)], limit=1, order='id asc') if not production_programming.programming_no: - production.fetchCNC(', '.join(product_id_to_production_names[production.product_id.id])) + production.fetchCNC(', '.join(product_id_to_production_names[production.product_id.id]), + scrap_production) else: production.write({'programming_no': production_programming.programming_no, 'programming_state': '编程中'}) + # # 根据加工面板的面数及对应的工序模板生成工单 i = 0 processing_panel_len = len(production.product_id.model_processing_panel.split(',')) @@ -406,20 +433,15 @@ class MrpProduction(models.Model): # 工单排序 def _reset_work_order_sequence1(self, k): - sequen = 0 for rec in self: - current_sequence = 10 + cnc_workorder = rec.workorder_ids.filtered(lambda wo: wo.name == "CNC加工") + cnc_back_workorder = rec.workorder_ids.filtered(lambda wo: wo.name == "CNC加工(返工)") for work in rec.workorder_ids: - work.sequence = current_sequence - current_sequence += 10 - if work.name == '后置三元质量检测' and work.processing_panel == k: - sequen = work.sequence - - for work in rec.workorder_ids: - if work.name == '后置三元质量检测(返工)' and work.processing_panel == k: - work.sequence = sequen + 2 - if work.name == 'CNC加工(返工)' and work.processing_panel == k: - work.sequence = sequen + 1 + if work.name == cnc_workorder.name and work.processing_panel == k: + cnc_back_workorder.write({'sequence': work.sequence + 1}) + print(cnc_back_workorder.sequence) + elif work.routing_type not in ['装夹预调'] and work != cnc_back_workorder: + work.sequence += 1 # 在制造订单上新增工单 def _create_workorder1(self, k): @@ -459,13 +481,7 @@ class MrpProduction(models.Model): order='sequence asc' ) i += 1 - for route in routingworkcenter: - - # if route.routing_type == '后置三元质量检测': - # workorders_values.append( - # self.env['mrp.workorder'].json_workorder_str1(k, production, route) - # ) if route.routing_type == 'CNC加工': workorders_values.append( self.env['mrp.workorder'].json_workorder_str1(k, production, route)) diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py index 1751a9e3..1d65c0ae 100644 --- a/sf_manufacturing/models/mrp_workorder.py +++ b/sf_manufacturing/models/mrp_workorder.py @@ -102,7 +102,7 @@ class ResMrpWorkOrder(models.Model): Z10_axis = fields.Float(default=0) X_deviation_angle = fields.Integer(string="X轴偏差度", default=0) test_results = fields.Selection([("合格", "合格"), ("返工", "返工"), ("报废", "报废")], default='合格', - string="检测结果") + string="检测结果", tracking=True) cnc_ids = fields.One2many("sf.cnc.processing", 'workorder_id', string="CNC加工程序") cmm_ids = fields.One2many("sf.cmm.program", 'workorder_id', string="CMM程序") tray_code = fields.Char(string="托盘编码") @@ -133,7 +133,7 @@ class ResMrpWorkOrder(models.Model): return action supplier_id = fields.Many2one('res.partner', string='外协供应商') - equipment_id = fields.Many2one('maintenance.equipment', string='加工设备') + equipment_id = fields.Many2one('maintenance.equipment', string='加工设备', tracking=True) is_ok = fields.Boolean(string='是否合格') # 加工人 processing_user_id = fields.Many2one('res.users', string='加工人') @@ -195,11 +195,16 @@ class ResMrpWorkOrder(models.Model): rfid_code_old = fields.Char('RFID码(已解除)') production_line_id = fields.Many2one('sf.production.line', related='production_id.production_line_id', - string='生产线', store=True) + string='生产线', store=True, tracking=True) production_line_state = fields.Selection(related='production_id.production_line_state', - string='上/下产线', store=True) + string='上/下产线', store=True, tracking=True) detection_report = fields.Binary('检测报告', readonly=True) - is_remanufacture = fields.Boolean(string='是否重新生成制造订单', default=True) + is_remanufacture = fields.Boolean(string='重新生成制造订单', default=False) + is_fetchcnc = fields.Boolean(string='重新获取NC程序', default=False) + reason = fields.Selection( + [("programming", "编程"), ("clamping", "返工"), ("cutter", "刀具"), ("operate computer", "操机"), + ("technology", "工艺"), ("customer redrawing", "客户改图"), ("other", "其他"), ], string="原因", tracking=True) + detailed_reason = fields.Text('详细原因') @api.onchange('rfid_code') def _onchange(self): @@ -672,18 +677,64 @@ class ResMrpWorkOrder(models.Model): """ 重新生成制造订单或者重新生成工单 """ - if self.test_results == '报废': + if self.test_results in ['返工', '报废']: values = self.env['mrp.production'].create_production1_values(self.production_id) productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company( self.production_id.company_id).create( values) - self.env['stock.move'].sudo().create(productions._get_moves_raw_values()) + # self.env['stock.move'].sudo().create(productions._get_moves_raw_values()) self.env['stock.move'].sudo().create(productions._get_moves_finished_values()) - productions._create_workorder() + productions._create_workorder(is_fetchcnc=self.is_fetchcnc, scrap_production=self.production_id) productions.filtered(lambda p: (not p.orderpoint_id and p.move_raw_ids) or \ ( p.move_dest_ids.procure_method != 'make_to_order' and not p.move_raw_ids and not p.workorder_ids)).action_confirm() + for production_item in productions: + process_parameter_workorder = self.env['mrp.workorder'].search( + [('surface_technics_parameters_id', '!=', False), ('production_id', '=', production_item.id), + ('is_subcontract', '=', True)]) + if process_parameter_workorder: + is_pick = False + consecutive_workorders = [] + m = 0 + sorted_workorders = sorted(process_parameter_workorder, key=lambda w: w.id) + for i in range(len(sorted_workorders) - 1): + if m == 0: + is_pick = False + if sorted_workorders[i].supplier_id.id == sorted_workorders[i + 1].supplier_id.id and \ + sorted_workorders[i].is_subcontract == sorted_workorders[i + 1].is_subcontract and \ + sorted_workorders[i].id == sorted_workorders[i + 1].id - 1: + if sorted_workorders[i] not in consecutive_workorders: + consecutive_workorders.append(sorted_workorders[i]) + consecutive_workorders.append(sorted_workorders[i + 1]) + m += 1 + continue + else: + if m == len(consecutive_workorders) - 1 and m != 0: + self.env['stock.picking'].create_outcontract_picking(consecutive_workorders, + production_item) + if sorted_workorders[i] in consecutive_workorders: + is_pick = True + consecutive_workorders = [] + m = 0 + # 当前面的连续工序生成对应的外协出入库单再生成当前工序的外协出入库单 + if is_pick is False: + self.env['stock.picking'].create_outcontract_picking(sorted_workorders[i], + production_item) + if m == len(consecutive_workorders) - 1 and m != 0: + self.env['stock.picking'].create_outcontract_picking(consecutive_workorders, + production_item) + if sorted_workorders[i] in consecutive_workorders: + is_pick = True + consecutive_workorders = [] + m = 0 + if m == len(consecutive_workorders) - 1 and m != 0: + self.env['stock.picking'].create_outcontract_picking(consecutive_workorders, production_item) + if is_pick is False and m == 0: + if len(sorted_workorders) == 1: + self.env['stock.picking'].create_outcontract_picking(sorted_workorders, production_item) + else: + self.env['stock.picking'].create_outcontract_picking(sorted_workorders[i], production_item) for production in productions: origin_production = production.move_dest_ids and production.move_dest_ids[ @@ -704,13 +755,36 @@ class ResMrpWorkOrder(models.Model): 'mail.message_origin_link', values={'self': production, 'origin': origin_production}, subtype_id=self.env.ref('mail.mt_note').id) - if self.test_results == '返工': - productions = self.production_id - # self.env['stock.move'].sudo().create(productions._get_moves_raw_values()) - # self.env['stock.move'].sudo().create(productions._get_moves_finished_values()) - productions._create_workorder2(self.processing_panel) - else: - self.results = '合格' + + ''' + 创建生产计划 + ''' + # 工单耗时 + workorder_duration = 0 + for workorder in productions.workorder_ids: + workorder_duration += workorder.duration_expected + + sale_order = self.env['sale.order'].sudo().search([('name', '=', productions.origin)]) + if sale_order: + sale_order.mrp_production_ids |= productions + # sale_order.write({'schedule_status': 'to schedule'}) + self.env['sf.production.plan'].sudo().with_company(self.production_id.company_id).create({ + 'name': productions.name, + 'order_deadline': sale_order.deadline_of_delivery, + 'production_id': productions.id, + 'date_planned_start': productions.date_planned_start, + 'origin': productions.origin, + 'product_qty': productions.product_qty, + 'product_id': productions.product_id.id, + 'state': 'draft', + }) + # if self.test_results == '返工': + # productions = self.production_id + # # self.env['stock.move'].sudo().create(productions._get_moves_raw_values()) + # # self.env['stock.move'].sudo().create(productions._get_moves_finished_values()) + # productions._create_workorder2(self.processing_panel) + # else: + # self.results = '合格' def json_workorder_str1(self, k, production, route): workorders_values_str = [0, '', { @@ -720,7 +794,6 @@ class ResMrpWorkOrder(models.Model): 'name': '%s(返工)' % route.route_workcenter_id.name, 'processing_panel': k, 'routing_type': route.routing_type, - 'work_state': '' if not route.routing_type == '获取CNC加工程序' else '待发起', 'workcenter_id': self.env['mrp.routing.workcenter'].get_workcenter(route.workcenter_ids.ids, route.routing_type, production.product_id), @@ -728,10 +801,37 @@ class ResMrpWorkOrder(models.Model): 'date_planned_finished': datetime.now() + timedelta(days=1), 'duration_expected': 60, 'duration': 0, - + 'manual_quotation': production.workorder_ids.filtered( + lambda t: t.routing_type == 'CNC加工').manual_quotation, + 'rfid_code': production.workorder_ids.filtered(lambda t: t.routing_type == 'CNC加工').rfid_code, + 'cnc_ids': production.workorder_ids.filtered(lambda t: t.routing_type == 'CNC加工').cnc_ids, + 'cmm_ids': production.workorder_ids.filtered(lambda t: t.routing_type == 'CNC加工').cmm_ids, }] return workorders_values_str + # @api.depends('production_availability', 'blocked_by_workorder_ids', 'blocked_by_workorder_ids.state') + # def _compute_state(self): + # super(ResMrpWorkOrder, self)._compute_state() + # for item in self: + # print(item.name) + # print(item.state) + # print(item.is_remanufacture) + # scrap_workorder = self.env['mrp.workorder'].search( + # [('production_id', '=', item.production_id.id), ('routing_type', '=', 'CNC加工'), + # ('state', '=', 'done'), ('test_results', 'in', ['返工', '报废'])]) + # print(scrap_workorder) + # # if item.routing_type == 'CNC加工' and item.state in ['done'] and item.test_results in ['返工', '报废']: + # if item.routing_type == '解除装夹': + # if scrap_workorder and item.state not in ['cancel']: + # item.state = 'cancel' + # elif item.routing_type == '表面工艺': + # if scrap_workorder: + # stock_move = self.env['stock.move'].search( + # [('origin', '=', item.production_id.name)]) + # stock_move.write({'state': 'cancel'}) + # item.picking_ids.write({'state': 'cancel'}) + # item.state = 'cancel' + # 重写工单开始按钮方法 def button_start(self): if self.routing_type == '装夹预调': @@ -887,6 +987,7 @@ class ResMrpWorkOrder(models.Model): raise UserError( '请先在产品中配置表面工艺为%s相关的外协服务产品' % item.surface_technics_parameters_id.name) tem_date_planned_finished = record.date_planned_finished + tem_date_finished = record.date_finished logging.info('routing_type:%s' % record.routing_type) super().button_finish() logging.info('date_planned_finished:%s' % record.date_planned_finished) @@ -895,6 +996,15 @@ class ResMrpWorkOrder(models.Model): record.write({ 'date_planned_finished': tem_date_planned_finished # 保持原值 }) + # if record.routing_type == 'CNC加工': + # record.write({ + # 'date_finished': tem_date_finished # 保持原值 + # }) + # if record.routing_type == 'CNC加工' and record.test_results in ['返工', '报废']: + # record.production_id.action_cancel() + # record.production_id.workorder_ids.write({'rfid_code': False, 'rfid_code_old': record.rfid_code}) + # if record.is_remanufacture is True: + # record.recreateManufacturingOrWorkerOrder() is_production_id = True for workorder in record.production_id.workorder_ids: if workorder.state != 'done': @@ -1035,8 +1145,10 @@ class CNCprocessing(models.Model): # 根据程序名和加工面匹配到ftp里对应的Nc程序名,可优化为根据cnc_processing.program_path进行匹配 def get_cnc_processing_file(self, serverdir, cnc_processing, program_path): logging.info('serverdir:%s' % serverdir) + logging.info('cnc_processing:%s' % cnc_processing) for root, dirs, files in os.walk(serverdir): for f in files: + logging.info('splitext(f):%s' % os.path.splitext(f)[1]) if os.path.splitext(f)[1] == ".pdf": full_path = os.path.join(serverdir, root, f) cnc_processing.workorder_id.cnc_worksheet = base64.b64encode( @@ -1206,17 +1318,18 @@ class SfWorkOrderBarcodes(models.Model): class WorkPieceDelivery(models.Model): _name = "sf.workpiece.delivery" + _inherit = ['mail.thread', 'mail.activity.mixin'] _description = '工件配送' - name = fields.Char('单据编号') + name = fields.Char('单据编码') workorder_id = fields.Many2one('mrp.workorder', string='工单', readonly=True) workorder_state = fields.Selection(related='workorder_id.state', string='工单状态') rfid_code = fields.Char(related='workorder_id.rfid_code', string='rfid码', store=True) production_id = fields.Many2one('mrp.production', string='制造订单号', readonly=True) - production_line_id = fields.Many2one('sf.production.line', string='目的生产线') + production_line_id = fields.Many2one('sf.production.line', string='目的生产线', tracking=True) plan_start_processing_time = fields.Datetime('计划开始加工时间', readonly=True) - route_id = fields.Many2one('sf.agv.task.route', '任务路线') + route_id = fields.Many2one('sf.agv.task.route', '任务路线', tracking=True) feeder_station_start_id = fields.Many2one('sf.agv.site', '起点接驳站') feeder_station_destination_id = fields.Many2one('sf.agv.site', '目的接驳站') task_delivery_time = fields.Datetime('任务下发时间') @@ -1225,26 +1338,41 @@ class WorkPieceDelivery(models.Model): [('上产线', '上产线'), ('下产线', '下产线'), ('运送空料架', '运送空料架')], string='类型') delivery_duration = fields.Float('配送时长', compute='_compute_delivery_duration') status = fields.Selection( - [('待下发', '待下发'), ('待配送', '待配送'), ('已配送', '已配送')], string='状态', default='待下发') - is_cnc_program_down = fields.Boolean('程序是否下发', default=False) + [('待下发', '待下发'), ('待配送', '待配送'), ('已配送', '已配送')], string='状态', default='待下发', + tracking=True) + is_cnc_program_down = fields.Boolean('程序是否下发', default=False, tracking=True) is_manual_work = fields.Boolean('人工操作', default=False) active = fields.Boolean(string="有效", default=True) @api.model def create(self, vals): - if vals.get('name', '/') == '/' or vals.get('name', '/') is False: - vals['name'] = self.env['ir.sequence'].next_by_code('sf.workpiece.delivery') or '/' - else: + if vals['route_id'] and vals.get('type') is None: vals['type'] = '运送空料架' + else: + if vals.get('name', '/') == '/' or vals.get('name', '/') is False: + vals['name'] = self.env['ir.sequence'].next_by_code('sf.workpiece.delivery') or '/' obj = super(WorkPieceDelivery, self).create(vals) + if obj.type == '运送空料架': + if obj.name is False: + obj.name = "运送空料架路线:%s-%s" % ( + obj.feeder_station_start_id.name, obj.feeder_station_destination_id.name) return obj - @api.constrains('name') - def _check_name(self): + @api.constrains('route_id') + def _check_route_id(self): if self.type == '运送空料架': - wd = self.sudo().search([('name', '=', self.name), ('id', '!=', self.id)]) - if wd: - raise UserError("该名称已存在") + if self.route_id and self.name is False: + route = self.sudo().search( + [('route_id', '=', self.route_id.id), ('id', '!=', self.id), ('name', 'ilike', '运送空料架路线')]) + if route: + raise UserError("该任务路线已存在,请重新选择") + + # @api.constrains('name') + # def _check_name(self): + # if self.type == '运送空料架': + # wd = self.sudo().search([('name', '=', self.name), ('id', '!=', self.id)]) + # if wd: + # raise UserError("该名称已存在") def action_delivery_history(self): return { diff --git a/sf_manufacturing/models/product_template.py b/sf_manufacturing/models/product_template.py index 7d9bbac5..0753d556 100644 --- a/sf_manufacturing/models/product_template.py +++ b/sf_manufacturing/models/product_template.py @@ -50,8 +50,8 @@ class ResProductMo(models.Model): cutting_tool_material_id = fields.Many2one('sf.cutting.tool.material', string='刀具物料') cutting_tool_type = fields.Char(string="刀具物料类型", related='cutting_tool_material_id.name', store=True) - cutting_tool_model_id = fields.Many2one('sf.cutting_tool.standard.library', string='型号') - specification_id = fields.Many2one('sf.tool.materials.basic.parameters', string='规格') + cutting_tool_model_id = fields.Many2one('sf.cutting_tool.standard.library', string='型号名称') + specification_id = fields.Many2one('sf.tool.materials.basic.parameters', string='物料号') cutting_tool_type_id = fields.Many2one('sf.cutting.tool.type', string='类型', domain="[('cutting_tool_material_id.name', '=', cutting_tool_type)]") @@ -104,6 +104,15 @@ class ResProductMo(models.Model): compaction_way_id = fields.Many2one('maintenance.equipment.image', '压紧方式', domain=[('type', '=', '压紧方式')]) + name = fields.Char('产品名称', compute='_compute_tool_name', store=True, required=False) + + @api.depends('cutting_tool_model_id', 'specification_id') + def _compute_tool_name(self): + for item in self: + if item.cutting_tool_model_id and item.specification_id: + name = '%s%s' % (item.cutting_tool_model_id.name, item.specification_id.name) + item.name = name + @api.onchange('cutting_tool_model_id') def _onchange_cutting_tool_model_id(self): for item in self: @@ -530,6 +539,7 @@ class ResProductMo(models.Model): # bfm下单 manual_quotation = fields.Boolean('人工编程', default=False, readonly=True) + part_number = fields.Char(string='零件图号', readonly=True) @api.constrains('tool_length') def _check_tool_length_size(self): @@ -626,6 +636,7 @@ class ResProductMo(models.Model): 'model_remark': item['remark'], 'default_code': '%s-%s' % (order_number, i), 'manual_quotation': item['manual_quotation'] or False, + 'part_number': item['part_number'] or '', 'active': True, } copy_product_id.sudo().write(vals) @@ -721,6 +732,10 @@ class ResProductMo(models.Model): logging.info('create-model_file:%s' % len(vals['model_file'])) self._sanitize_vals(vals) templates = super(ResProductMo, self).create(vals_list) + # 产品名称唯一性校验 + for item in templates: + if len(self.search([('name', '=', item.name)])) > 1: + raise ValidationError('产品名称【%s】已存在' % item.name) if "create_product_product" not in self._context: templates._create_variant_ids() diff --git a/sf_manufacturing/models/stock.py b/sf_manufacturing/models/stock.py index 6874dd22..03ec8d0e 100644 --- a/sf_manufacturing/models/stock.py +++ b/sf_manufacturing/models/stock.py @@ -3,6 +3,7 @@ import base64 import qrcode from collections import defaultdict, namedtuple import logging +import io import json from re import split as regex_split from re import findall as regex_findall @@ -207,7 +208,7 @@ class StockRule(models.Model): ''' 创建工单 ''' - productions._create_workorder() + productions._create_workorder(is_fetchcnc=False, scrap_production=False) productions.filtered(lambda p: (not p.orderpoint_id and p.move_raw_ids) or \ ( @@ -631,8 +632,8 @@ class ReStockMove(models.Model): 'reserved_uom_qty': 1.0, 'lot_id': purchase.picking_ids.move_line_ids.lot_id.id, 'company_id': self.company_id.id, - 'workorder_id': '' if not sorted_workorders else sorted_workorders.id, - 'production_id': '' if not sorted_workorders else sorted_workorders.production_id.id, + # 'workorder_id': '' if not sorted_workorders else sorted_workorders.id, + # 'production_id': '' if not sorted_workorders else sorted_workorders.production_id.id, 'state': 'assigned', } @@ -668,12 +669,14 @@ class ReStockMove(models.Model): else: view = self.env.ref('stock.view_stock_move_nosuggest_operations') - if self.product_id.tracking == "serial" and self.state == "assigned": - print(self.origin) - if self.product_id.categ_id.name == '刀具': - self.next_serial = self._get_tool_next_serial(self.company_id, self.product_id, self.origin) - else: - self.next_serial = self.env['stock.lot']._get_next_serial(self.company_id, self.product_id) + if self.state == "assigned": + if self.product_id.tracking == "serial": + if self.product_id.categ_id.name == '刀具': + self.next_serial = self._get_tool_next_serial(self.company_id, self.product_id, self.origin) + else: + self.next_serial = self.env['stock.lot']._get_next_serial(self.company_id, self.product_id) + elif self.product_id.tracking == "lot": + self._put_tool_lot(self.company_id, self.product_id, self.origin) return { 'name': _('Detailed Operations'), @@ -698,6 +701,35 @@ class ReStockMove(models.Model): ), } + def _put_tool_lot(self, company, product, origin): + if product.tracking == "lot" and self.product_id.categ_id.name == '刀具': + if not self.move_line_nosuggest_ids: + lot_names = self.env['stock.lot'].generate_lot_names( + '%s-%s-%s' % ('%s-T-DJWL-%s' % ( + product.cutting_tool_model_id.code.split('-')[0], product.cutting_tool_material_id.code), + datetime.now().strftime("%Y%m%d"), origin), 1) + move_lines_commands = self._generate_serial_move_line_commands_tool_lot(lot_names) + self.write({'move_line_nosuggest_ids': move_lines_commands}) + for item in self.move_line_nosuggest_ids: + if item.lot_name: + item.lot_qr_code = self.compute_lot_qr_code(item.lot_name) + + def compute_lot_qr_code(self, lot_name): + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(lot_name) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + buffer = io.BytesIO() + img.save(buffer, format="PNG") + binary_data = buffer.getvalue() + data = base64.b64encode(binary_data).decode() # 确保返回的是字符串形式的数据 + return data + def _get_tool_next_serial(self, company, product, origin): """Return the next serial number to be attributed to the product.""" if product.tracking == "serial": @@ -711,6 +743,67 @@ class ReStockMove(models.Model): else: return "%s-T-%s-%s-%03d" % (split_codes[0], origin, product.specification_id.name, 1) + def _generate_serial_move_line_commands_tool_lot(self, lot_names, origin_move_line=None): + """Return a list of commands to update the move lines (write on + existing ones or create new ones). + Called when user want to create and assign multiple serial numbers in + one time (using the button/wizard or copy-paste a list in the field). + + :param lot_names: A list containing all serial number to assign. + :type lot_names: list + :param origin_move_line: A move line to duplicate the value from, default to None + :type origin_move_line: record of :class:`stock.move.line` + :return: A list of commands to create/update :class:`stock.move.line` + :rtype: list + """ + self.ensure_one() + + # Select the right move lines depending of the picking type configuration. + move_lines = self.env['stock.move.line'] + if self.picking_type_id.show_reserved: + move_lines = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_name) + else: + move_lines = self.move_line_nosuggest_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_name) + + loc_dest = origin_move_line and origin_move_line.location_dest_id + move_line_vals = { + 'picking_id': self.picking_id.id, + 'location_id': self.location_id.id, + 'product_id': self.product_id.id, + 'product_uom_id': self.product_id.uom_id.id, + 'qty_done': self.product_uom_qty, + } + if origin_move_line: + # `owner_id` and `package_id` are taken only in the case we create + # new move lines from an existing move line. Also, updates the + # `qty_done` because it could be usefull for products tracked by lot. + move_line_vals.update({ + 'owner_id': origin_move_line.owner_id.id, + 'package_id': origin_move_line.package_id.id, + 'qty_done': origin_move_line.qty_done or 1, + }) + + move_lines_commands = [] + qty_by_location = defaultdict(float) + for lot_name in lot_names: + # We write the lot name on an existing move line (if we have still one)... + if move_lines: + move_lines_commands.append((1, move_lines[0].id, { + 'lot_name': lot_name, + 'qty_done': 1, + })) + qty_by_location[move_lines[0].location_dest_id.id] += 1 + move_lines = move_lines[1:] + # ... or create a new move line with the serial name. + else: + loc = loc_dest or self.location_dest_id._get_putaway_strategy(self.product_id, quantity=1, + packaging=self.product_packaging_id, + additional_qty=qty_by_location) + move_line_cmd = dict(move_line_vals, lot_name=lot_name, location_dest_id=loc.id) + move_lines_commands.append((0, 0, move_line_cmd)) + qty_by_location[loc.id] += 1 + return move_lines_commands + class ReStockQuant(models.Model): _inherit = 'stock.quant' diff --git a/sf_manufacturing/views/agv_setting_views.xml b/sf_manufacturing/views/agv_setting_views.xml index 42662e2c..377bee74 100644 --- a/sf_manufacturing/views/agv_setting_views.xml +++ b/sf_manufacturing/views/agv_setting_views.xml @@ -7,9 +7,9 @@ sf.agv.site - - - + + + @@ -34,12 +34,14 @@ sf.agv.task.route - + - - + + - + @@ -73,7 +75,8 @@ center_control.interface.log - + + diff --git a/sf_manufacturing/views/mrp_production_addional_change.xml b/sf_manufacturing/views/mrp_production_addional_change.xml index c5616242..55dbbe33 100644 --- a/sf_manufacturing/views/mrp_production_addional_change.xml +++ b/sf_manufacturing/views/mrp_production_addional_change.xml @@ -87,7 +87,7 @@ - + diff --git a/sf_manufacturing/views/mrp_workorder_view.xml b/sf_manufacturing/views/mrp_workorder_view.xml index bc1d4bfe..11219363 100644 --- a/sf_manufacturing/views/mrp_workorder_view.xml +++ b/sf_manufacturing/views/mrp_workorder_view.xml @@ -80,7 +80,7 @@ tree,form - + current [('state', '!=', 'cancel'),('schedule_state', '=', '已排')] @@ -214,7 +214,7 @@ attrs='{"invisible": [("routing_type","!=","装夹预调")]}'/> - @@ -477,8 +477,13 @@ - - + + + + + + + @@ -573,7 +578,7 @@ @@ -604,7 +609,7 @@ 工件配送 sf.workpiece.delivery - +
@@ -612,13 +617,13 @@ decoration-success="status == '已配送'" decoration-warning="status == '待下发'" decoration-danger="status == '待配送'"/> + - @@ -629,13 +634,58 @@ + + 工件配送 + sf.workpiece.delivery + +
+
+ +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ 工件配送 sf.workpiece.delivery - - + + + @@ -657,8 +707,10 @@ 工件配送 sf.workpiece.delivery - {'search_default_on_up':1} - tree,search + {'search_default_filter_to_be_issued': 1, + 'search_default_filter_waiting_delivery': 1} + + tree,form [('type','in',['上产线','下产线']),('workorder_state','=','done'),('is_manual_work','=',false)] @@ -671,23 +723,39 @@ sf.workpiece.delivery -
-
- + + + + + +