diff --git a/sf_base/models/sf_base.py b/sf_base/models/sf_base.py new file mode 100644 index 00000000..e337d301 --- /dev/null +++ b/sf_base/models/sf_base.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +import logging +from datetime import datetime + +import requests +import json +from odoo import fields, models, api +from odoo.exceptions import ValidationError +from odoo.http import request +from odoo.addons.sf_base.commons.common import Common + +_logger = logging.getLogger(__name__) + + +class MachineBrandTags(models.Model): + _name = 'mrs.machine.brand.tags' + _description = '标签' + name = fields.Char('名称', size=50) + color = fields.Integer('颜色', default=0) + + +class MachineControlSystem(models.Model): + _name = 'mrs.machine.control_system' + _description = '控制系统' + + code = fields.Char('编码', size=10) + name = fields.Char('名称', size=10) + brand_id = fields.Many2one('mrs.machine.brand', '品牌') + active = fields.Boolean('有效', default=True) + + +# 品牌标签 +class MachineBrand(models.Model): + _name = 'mrs.machine.brand' + _description = '品牌' + + name = fields.Char('名称') + tag_ids = fields.Many2many('mrs.machine.brand.tags', 'rel_machine_brand_tags', string='类别') + image_brand = fields.Image("品牌图片") + active = fields.Boolean('有效', default=True) + code = fields.Char('编码') + + +# 机床 +class MachineTool(models.Model): + _name = 'mrs.machine_tool' + _description = '机床' + MTcode = fields.Char("编码") + code = fields.Char('行业编码') + name = fields.Char('名称') + knife_type = fields.Selection( + [("BT40", "BT40"), ("BT30", "BT30")], + default="", string="刀把类型") + number_of_knife_library = fields.Integer('刀库数量') + rotate_speed = fields.Integer('转速') + number_of_axles = fields.Selection( + [("三轴", "三轴"), ("四轴", "四轴"), ("五轴", "五轴")], + default="", string="轴数") + # 加工进程 + x_axis = fields.Integer('X轴') + y_axis = fields.Integer('Y轴') + z_axis = fields.Integer('Z轴') + b_axis = fields.Integer('B轴') + c_axis = fields.Integer('C轴') + remark = fields.Text('备注') + precision = fields.Float('加工精度') + control_system_id = fields.Many2one('mrs.machine.control_system', + string="控制系统") + # 多个机床型号对应一个机床 + type_id = fields.Many2one('mrs.machine_tool.type', '型号') + brand_id = fields.Many2one('mrs.machine.brand', string='品牌') + state = fields.Selection( + [("正常", "正常"), ("故障", "故障"), ("不可用", "不可用")], + default='正常', string="状态") + + # 一个机床对应一個加工工厂,一个加工工厂对应多个机床 + factory_id = fields.Many2one('res.partner', string='所属工厂', + domain="[('is_factory', '=', True)]") + # 一个机床对应一个供应商,一个供应商对应多个机床 + supplier_id = fields.Many2one('res.partner', string='制造商', + domain="[('is_vendor', '=', True)]") + registration_date = fields.Date('注册日期') + + active = fields.Boolean('有效', default=True) + + @api.constrains('rotate_speed') + def _check_rotate_speed(self): + if self.rotate_speed <= 0: + raise ValidationError("转速不能为0") + + @api.constrains('precision') + def _check_precision(self): + if self.precision <= 0.00: + raise ValidationError("加工精度不能为0") + + @api.constrains('number_of_knife_library') + def _check_number_of_knife_library(self): + if self.number_of_knife_library <= 0: + raise ValidationError("刀库数量不能为0") + + @api.constrains('x_axis') + def _check_x_axis(self): + if self.x_axis <= 0: + raise ValidationError("加工行程里x轴不能为0") + + @api.constrains('y_axis') + def _check_y_axis(self): + if self.y_axis <= 0: + raise ValidationError("加工行程里y轴不能为0") + + @api.constrains('z_axis') + def _check_z_axis(self): + if self.z_axis <= 0: + raise ValidationError("加工行程里z轴不能为0") + + @api.constrains('b_axis') + def _check_b_axis(self): + if self.number_of_axles == '四轴': + print(self.number_of_axles) + if self.b_axis <= 0: + raise ValidationError("加工行程里b轴不能为0") + + @api.constrains('c_axis') + def _check_c_axis(self): + if self.number_of_axles == '五轴': + if self.c_axis <= 0: + raise ValidationError("加工行程里c轴不能为0") + + @api.onchange('type_id') + def get_type_info(self): + for item in self: + item.knife_type = item.type_id.knife_type + item.number_of_knife_library = item.type_id.number_of_knife_library + item.number_of_axles = item.type_id.number_of_axles + item.rotate_speed = item.type_id.rotate_speed + item.precision = item.type_id.precision + item.control_system_id = item.type_id.control_system_id + item.x_axis = item.type_id.x_axis + item.y_axis = item.type_id.y_axis + item.z_axis = item.type_id.z_axis + item.b_axis = item.type_id.b_axis + item.c_axis = item.type_id.c_axis + + # 注册同步机床 + def enroll_machine_tool(self): + sf_sync_config = self.env['res.config.settings'].get_values() + token = sf_sync_config['token'] + mrs_secret_key = sf_sync_config['mrs_secret_key'] + headers = Common.get_headers(self, token, mrs_secret_key) + strurl = sf_sync_config['mrs_url'] + self.crea_url + objs_all = request.env['mrs.machine_tool'].sudo().search([]) + machine_tool_list = [] + if objs_all: + for item in objs_all: + val = { + 'factory_token': token, + 'id': item.id, + 'name': item.name, + 'code': item.code, + 'precision': item.precision, + 'knife_type': item.knife_type, + 'number_of_knife_library': item.number_of_knife_library, + 'rotate_speed': item.rotate_speed, + 'number_of_axles': item.number_of_axles, + 'control_system_id': self.env['mrs.machine.control_system'].search( + [('id', '=', item.control_system_id.id)]).code, + 'type_id': self.env['mrs.machine_tool.type'].search([('id', '=', item.type_id.id)]).code, + 'brand_id': self.env['mrs.machine.brand'].search([('id', '=', item.brand_id.id)]).code, + 'supplier_id': item.supplier_id.id, + 'x_axis': item.x_axis, + 'y_axis': item.y_axis, + 'z_axis': item.z_axis, + 'b_axis': item.b_axis, + 'c_axis': item.c_axis, + 'state': item.state, + 'active': item.active, + + } + machine_tool_list.append(val) + # kw = machine_tool_list + kw = json.dumps(machine_tool_list, ensure_ascii=False) + r = requests.post(strurl, json={}, data={'kw': kw}, headers=headers) + print(r) + if r == 200: + raise ValidationError("机床注册成功") + else: + raise ValidationError("没有注册机床信息") + + +class MachineToolType(models.Model): + _name = 'mrs.machine_tool.type' + _description = '机床型号' + # _order = 'priority desc, code, name, id' + + name = fields.Char('名称') + brand_id = fields.Many2one('mrs.machine.brand', string='品牌') + knife_type = fields.Selection( + [("BT40", "BT40"), ("BT30", "BT30")], + default="", string="刀把类型") + number_of_knife_library = fields.Integer('刀库数量') + rotate_speed = fields.Integer('转速') + # 多个型号对应一个机床 + machine_tool_id = fields.Many2one('mrs.machine_tool', '机床') + number_of_axles = fields.Selection( + [("三轴", "三轴"), ("四轴", "四轴"), ("五轴", "五轴")], + default="", string="轴数") + # 加工进程 + x_axis = fields.Integer('X轴') + y_axis = fields.Integer('Y轴') + z_axis = fields.Integer('Z轴') + b_axis = fields.Integer('B轴') + c_axis = fields.Integer('C轴') + remark = fields.Text('备注') + precision = fields.Float('加工精度') + control_system_id = fields.Many2one('mrs.machine.control_system', + string="控制系统") + active = fields.Boolean('有效', default=True) + code = fields.Char('编码') + + +# 刀具 +class CuttingTool(models.Model): + _name = 'mrs.cutting_tool.category' + _description = '刀具类别' + + code = fields.Char('编码') + name = fields.Char('名称') + + remark = fields.Text('备注') + active = fields.Boolean('有效', default=True) + + +class CuttingToolType(models.Model): + _name = 'mrs.cutting_tool.type' + _description = '刀具型号' + + code = fields.Char('编码') + name = fields.Char('名称') + diameter = fields.Integer('直径') + long_blade = fields.Integer('避空长/刃长') + cone_angle_pitch = fields.Integer('锥角/节距') + shank_diameter = fields.Integer('柄径') + taper_shank_length = fields.Integer('锥柄长') + tool_length = fields.Integer('刀具总长') + blade_number = fields.Integer('刃数') + category_id = fields.Many2one('mrs.cutting_tool.category', string='刀具类别') + brand_id = fields.Many2one('mrs.machine.brand', string='品牌') + remark = fields.Text('备注') + active = fields.Boolean('有效', default=True) + + +class CNCprocessing(models.Model): + _name = 'cnc.processing' + _description = "CNC加工" + + cnc_id = fields.Many2one('ir.attachment') + FNo = fields.Char(string="序号") + FPGName = fields.Char(string="程序名") + FKnifeName = fields.Char(string="刀具名称") + FDNo = fields.Char(string="刀号") + FWorkType = fields.Char(string="加工类型") + FXY = fields.Char(string="余量_X/Y") + FZ = fields.Char(string="余量_Z") + FJGSD = fields.Char(string="加工深度(Z)") + FSCCD = fields.Char(string="刀具伸出长度") + FDJSpec = fields.Char(string="刀柄型号") + FJGDate = fields.Char(string="预计加工时间") + FComment = fields.Char(string="备注") diff --git a/sf_base/views/mrs_common_view.xml b/sf_base/views/mrs_common_view.xml index 748b0683..fa9cfba1 100644 --- a/sf_base/views/mrs_common_view.xml +++ b/sf_base/views/mrs_common_view.xml @@ -296,9 +296,8 @@ - - + @@ -338,5 +337,6 @@ + \ No newline at end of file diff --git a/sf_manufacturing_orders/__manifest__.py b/sf_manufacturing_orders/__manifest__.py new file mode 100644 index 00000000..4bcec36c --- /dev/null +++ b/sf_manufacturing_orders/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': '机企猫智能工厂 制造订单', + 'version': '1.0', + 'summary': '智能工厂制造订单', + 'sequence': 1, + 'description': """ + + """, + 'category': '', + 'website': 'https://www.sf.jikimo.com', + 'depends': ['mrp'], + 'data': [ + 'views/sf_production.xml', + + + ], + 'demo': [ + ], + 'qweb': [ + ], + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/sf_manufacturing_orders/models/sf_production.py b/sf_manufacturing_orders/models/sf_production.py index 764f5d33..a41e34f3 100644 --- a/sf_manufacturing_orders/models/sf_production.py +++ b/sf_manufacturing_orders/models/sf_production.py @@ -203,115 +203,59 @@ class StockRule(models.Model): return True - # @api.model - # def _run_manufacture(self, procurements): - # productions_values_by_company = defaultdict(list) - # errors = [] - # for procurement, rule in procurements: - # if float_compare(procurement.product_qty, 0, precision_rounding=procurement.product_uom.rounding) <= 0: - # # If procurement contains negative quantity, don't create a MO that would be for a negative value. - # continue - # bom = rule._get_matching_bom(procurement.product_id, procurement.company_id, procurement.values) - # - # product = self.env['product.template'].search( - # ["&", ("id", '=', procurement.product_id.id), ('single_manufacturing', '!=', False)]) - # if not product: - # productions_values_by_company[procurement.company_id.id].append( - # rule._prepare_mo_vals1(*procurement, bom)) - # else: - # orders_vals = rule._prepare_mo_vals(*procurement, bom) - # - # if isinstance(orders_vals, list): - # for vals in orders_vals: - # productions_values_by_company[procurement.company_id.id].append(vals) - # else: - # productions_values_by_company[procurement.company_id.id].append(orders_vals) - # - # if errors: - # raise ProcurementException(errors) - # - # for company_id, productions_values in productions_values_by_company.items(): - # # create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) - # productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create( - # productions_values) - # a = self.env['stock.move'].sudo().create(productions._get_moves_raw_values()) - # b = self.env['stock.move'].sudo().create(productions._get_moves_finished_values()) - # c = productions._create_workorder() - # 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 in productions: - # origin_production = production.move_dest_ids and production.move_dest_ids[ - # 0].raw_material_production_id or False - # orderpoint = production.orderpoint_id - # if orderpoint and orderpoint.create_uid.id == SUPERUSER_ID and orderpoint.trigger == 'manual': - # production.message_post( - # body=_('This production order has been created from Replenishment Report.'), - # message_type='comment', - # subtype_xmlid='mail.mt_note') - # elif orderpoint: - # production.message_post_with_view( - # 'mail.message_origin_link', - # values={'self': production, 'origin': orderpoint}, - # subtype_id=self.env.ref('mail.mt_note').id) - # elif origin_production: - # production.message_post_with_view( - # 'mail.message_origin_link', - # values={'self': production, 'origin': origin_production}, - # subtype_id=self.env.ref('mail.mt_note').id) - # return True - # - # def _prepare_mo_vals1(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values, - # bom): - # date_planned = self._get_date_planned(product_id, company_id, values) - # date_deadline = values.get('date_deadline') or date_planned + relativedelta( - # days=company_id.manufacturing_lead) + relativedelta(days=product_id.produce_delay) - # mo_values = { - # 'origin': origin, - # 'product_id': product_id.id, - # 'product_description_variants': values.get('product_description_variants'), - # 'product_qty': product_qty, - # 'product_uom_id': product_uom.id, - # 'location_src_id': self.location_src_id.id or self.picking_type_id.default_location_src_id.id or location_id.id, - # 'location_dest_id': location_id.id, - # 'bom_id': bom.id, - # 'date_deadline': date_deadline, - # 'date_planned_start': date_planned, - # 'date_planned_finished': fields.Datetime.from_string(values['date_planned']), - # 'procurement_group_id': False, - # 'propagate_cancel': self.propagate_cancel, - # 'orderpoint_id': values.get('orderpoint_id', False) and values.get('orderpoint_id').id, - # 'picking_type_id': self.picking_type_id.id or values['warehouse_id'].manu_type_id.id, - # 'company_id': company_id.id, - # 'move_dest_ids': values.get('move_dest_ids') and [(4, x.id) for x in values['move_dest_ids']] or False, - # 'user_id': False, - # } - # # Use the procurement group created in _run_pull mrp override - # # Preserve the origin from the original stock move, if available - # if location_id.warehouse_id.manufacture_steps == 'pbm_sam' and values.get('move_dest_ids') and values.get( - # 'group_id') and values['move_dest_ids'][0].origin != values['group_id'].name: - # origin = values['move_dest_ids'][0].origin - # mo_values.update({ - # 'name': values['group_id'].name, - # 'procurement_group_id': values['group_id'].id, - # 'origin': origin, - # }) - # return mo_values - # - # def _prepare_mo_vals(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values, - # bom): - # - # mo_lists = [] - # for num in range(1, int(product_qty) + 1): - # vals = super(StockRule, self)._prepare_mo_vals(product_id, 1.0, product_uom, location_id, name, origin, - # company_id, values, - # bom) - # vals.update({ - # # 'name': values['group_id'].name, - # 'procurement_group_id': values['group_id'].id, - # # 'origin': origin, - # }) - # mo_lists.append(vals) - # - # return mo_lists + @api.model + def _run_manufacture(self, procurements): + productions_values_by_company = defaultdict(list) + errors = [] + for procurement, rule in procurements: + if float_compare(procurement.product_qty, 0, precision_rounding=procurement.product_uom.rounding) <= 0: + # If procurement contains negative quantity, don't create a MO that would be for a negative value. + continue + bom = rule._get_matching_bom(procurement.product_id, procurement.company_id, procurement.values) + + productions_values_by_company[procurement.company_id.id].append(rule._prepare_mo_vals(*procurement, bom)) + + if errors: + raise ProcurementException(errors) + + for company_id, productions_values in productions_values_by_company.items(): + # create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) + '''创建制造订单''' + productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create( + productions_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.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 in productions: + ''' + 创建制造订单时生成序列号 + ''' + production.lot_producing_id._get_next_serial(production.company_id, production.product_id) + production.action_generate_serial() + origin_production = production.move_dest_ids and production.move_dest_ids[ + 0].raw_material_production_id or False + orderpoint = production.orderpoint_id + if orderpoint and orderpoint.create_uid.id == SUPERUSER_ID and orderpoint.trigger == 'manual': + production.message_post( + body=_('This production order has been created from Replenishment Report.'), + message_type='comment', + subtype_xmlid='mail.mt_note') + elif orderpoint: + production.message_post_with_view( + 'mail.message_origin_link', + values={'self': production, 'origin': orderpoint}, + subtype_id=self.env.ref('mail.mt_note').id) + elif origin_production: + production.message_post_with_view( + 'mail.message_origin_link', + values={'self': production, 'origin': origin_production}, + subtype_id=self.env.ref('mail.mt_note').id) + return True \ No newline at end of file diff --git a/sf_route_workcenter/__manifest__.py b/sf_route_workcenter/__manifest__.py index 6cf239f2..5d8e16ca 100644 --- a/sf_route_workcenter/__manifest__.py +++ b/sf_route_workcenter/__manifest__.py @@ -10,10 +10,13 @@ """, 'category': 'YZ', 'website': 'https://www.sf.cs.jikimo.com', - 'depends': ['mrp', 'sf_base','hr_holidays'], + 'depends': ['mrp', 'sf_base', 'hr_holidays', 'maintenance'], 'data': [ 'views/sf_tray_view.xml', 'views/sf_workorder.xml', + 'views/maintenance_views.xml', + 'views/mrp_views.xml', + 'views/mrp_workcenter_views.xml', 'report/sf_tray_report.xml' ], 'demo': [ diff --git a/sf_route_workcenter/models/__init__.py b/sf_route_workcenter/models/__init__.py index 94d72e99..f5061dc2 100644 --- a/sf_route_workcenter/models/__init__.py +++ b/sf_route_workcenter/models/__init__.py @@ -1,2 +1,3 @@ # -*-coding:utf-8-*- -from . import workcenter \ No newline at end of file +from . import workcenter +from . import mrp_maintenance diff --git a/sf_route_workcenter/models/mrp_maintenance.py b/sf_route_workcenter/models/mrp_maintenance.py new file mode 100644 index 00000000..d0ea9b42 --- /dev/null +++ b/sf_route_workcenter/models/mrp_maintenance.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from dateutil.relativedelta import relativedelta +from datetime import timedelta, datetime +from collections import defaultdict + +from odoo import api, fields, models, _ +from odoo.addons.resource.models.resource import Intervals + + + + + +class MrpWorkcenter(models.Model): + _inherit = "mrp.workcenter" + + equipment_ids = fields.One2many( + 'maintenance.equipment', 'workcenter_id', string="Maintenance Equipment", + check_company=True) + + def action_work_order(self): + if not self.env.context.get('desktop_list_view', False): + action = self.env["ir.actions.actions"]._for_xml_id("sf_route_workcenter.mrp_workorder_action_tablet") + return action + else: + return super(MrpWorkcenter, self).action_work_order() + + def _get_unavailability_intervals(self, start_datetime, end_datetime): + res = super(MrpWorkcenter, self)._get_unavailability_intervals(start_datetime, end_datetime) + if not self: + return res + sql = """ + SELECT workcenter_id, ARRAY_AGG((schedule_date || '|' || schedule_date + INTERVAL '1h' * duration)) as date_intervals + FROM maintenance_request + LEFT JOIN maintenance_equipment + ON maintenance_request.equipment_id = maintenance_equipment.id + WHERE + schedule_date IS NOT NULL + AND duration IS NOT NULL + AND equipment_id IS NOT NULL + AND maintenance_equipment.workcenter_id IS NOT NULL + AND maintenance_equipment.workcenter_id IN %s + AND (schedule_date, schedule_date + INTERVAL '1h' * duration) OVERLAPS (%s, %s) + GROUP BY maintenance_equipment.workcenter_id; + """ + self.env.cr.execute(sql, [tuple(self.ids), fields.Datetime.to_string(start_datetime.astimezone()), fields.Datetime.to_string(end_datetime.astimezone())]) + res_maintenance = defaultdict(list) + for wc_row in self.env.cr.dictfetchall(): + res_maintenance[wc_row.get('workcenter_id')] = [ + [fields.Datetime.to_datetime(i) for i in intervals.split('|')] + for intervals in wc_row.get('date_intervals') + ] + + for wc_id in self.ids: + intervals_previous_list = [(s.timestamp(), e.timestamp(), self.env['maintenance.request']) for s, e in res[wc_id]] + intervals_maintenances_list = [(m[0].timestamp(), m[1].timestamp(), self.env['maintenance.request']) for m in res_maintenance[wc_id]] + final_intervals_wc = Intervals(intervals_previous_list + intervals_maintenances_list) + res[wc_id] = [(datetime.fromtimestamp(s), datetime.fromtimestamp(e)) for s, e, _ in final_intervals_wc] + return res + + +class MaintenanceEquipment(models.Model): + _inherit = "maintenance.equipment" + _check_company_auto = True + + expected_mtbf = fields.Integer(string='Expected MTBF', help='Expected Mean Time Between Failure') + mtbf = fields.Integer(compute='_compute_maintenance_request', string='MTBF', help='Mean Time Between Failure, computed based on done corrective maintenances.') + mttr = fields.Integer(compute='_compute_maintenance_request', string='MTTR', help='Mean Time To Repair') + estimated_next_failure = fields.Date(compute='_compute_maintenance_request', string='Estimated time before next failure (in days)', help='Computed as Latest Failure Date + MTBF') + latest_failure_date = fields.Date(compute='_compute_maintenance_request', string='Latest Failure Date') + workcenter_id = fields.Many2one( + 'mrp.workcenter', string='Work Center', check_company=True) + + @api.depends('effective_date', 'maintenance_ids.stage_id', 'maintenance_ids.close_date', 'maintenance_ids.request_date') + def _compute_maintenance_request(self): + for equipment in self: + maintenance_requests = equipment.maintenance_ids.filtered(lambda x: x.maintenance_type == 'corrective' and x.stage_id.done) + mttr_days = 0 + for maintenance in maintenance_requests: + if maintenance.stage_id.done and maintenance.close_date: + mttr_days += (maintenance.close_date - maintenance.request_date).days + equipment.mttr = len(maintenance_requests) and (mttr_days / len(maintenance_requests)) or 0 + maintenance = maintenance_requests.sorted(lambda x: x.request_date) + if len(maintenance) >= 1: + equipment.mtbf = (maintenance[-1].request_date - equipment.effective_date).days / len(maintenance) + equipment.latest_failure_date = maintenance and maintenance[-1].request_date or False + if equipment.mtbf: + equipment.estimated_next_failure = equipment.latest_failure_date + relativedelta(days=equipment.mtbf) + else: + equipment.estimated_next_failure = False + + def button_mrp_workcenter(self): + self.ensure_one() + return { + 'name': _('work centers'), + 'view_mode': 'form', + 'res_model': 'mrp.workcenter', + 'view_id': self.env.ref('mrp.mrp_workcenter_view').id, + 'type': 'ir.actions.act_window', + 'res_id': self.workcenter_id.id, + 'context': { + 'default_company_id': self.company_id.id + } + } + + +class MaintenanceRequest(models.Model): + _inherit = "maintenance.request" + _check_company_auto = True + + production_id = fields.Many2one( + 'mrp.production', string='Manufacturing Order', check_company=True) + workorder_id = fields.Many2one( + 'mrp.workorder', string='Work Order', check_company=True) + production_company_id = fields.Many2one(string='Production Company', related='production_id.company_id') + company_id = fields.Many2one(domain="[('id', '=?', production_company_id)]") + + +class MrpProduction(models.Model): + _inherit = "mrp.production" + + maintenance_count = fields.Integer(compute='_compute_maintenance_count', string="Number of maintenance requests") + request_ids = fields.One2many('maintenance.request', 'production_id') + + @api.depends('request_ids') + def _compute_maintenance_count(self): + for production in self: + production.maintenance_count = len(production.request_ids) + + def button_maintenance_req(self): + self.ensure_one() + return { + 'name': _('New Maintenance Request'), + 'view_mode': 'form', + 'res_model': 'maintenance.request', + 'type': 'ir.actions.act_window', + 'context': { + 'default_company_id': self.company_id.id, + 'default_production_id': self.id, + }, + 'domain': [('production_id', '=', self.id)], + } + + def open_maintenance_request_mo(self): + self.ensure_one() + action = { + 'name': _('Maintenance Requests'), + 'view_mode': 'kanban,tree,form,pivot,graph,calendar', + 'res_model': 'maintenance.request', + 'type': 'ir.actions.act_window', + 'context': { + 'default_company_id': self.company_id.id, + 'default_production_id': self.id, + }, + 'domain': [('production_id', '=', self.id)], + } + if self.maintenance_count == 1: + production = self.env['maintenance.request'].search([('production_id', '=', self.id)]) + action['view_mode'] = 'form' + action['res_id'] = production.id + return action + + +class MrpProductionWorkcenterLine(models.Model): + _inherit = "mrp.workorder" + + def button_maintenance_req(self): + self.ensure_one() + return { + 'name': _('New Maintenance Request'), + 'view_mode': 'form', + 'views': [(self.env.ref('mrp_maintenance.maintenance_request_view_form_inherit_mrp').id, 'form')], + 'res_model': 'maintenance.request', + 'type': 'ir.actions.act_window', + 'context': { + 'default_company_id': self.company_id.id, + 'default_workorder_id': self.id, + 'default_production_id': self.production_id.id, + 'discard_on_footer_button': True, + }, + 'target': 'new', + 'domain': [('workorder_id', '=', self.id)] + } diff --git a/sf_route_workcenter/models/workcenter.py b/sf_route_workcenter/models/workcenter.py new file mode 100644 index 00000000..25a8b5b2 --- /dev/null +++ b/sf_route_workcenter/models/workcenter.py @@ -0,0 +1,339 @@ +# -*- coding: utf-8 -*- +# Part of SmartGo. See LICENSE file for full copyright and licensing details. +import base64 +import logging +import math + +from io import BytesIO +from odoo import api, fields, models, SUPERUSER_ID +from pystrich.code128 import Code128Encoder +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class CNCprocessing(models.Model): + _inherit = 'cnc.processing' + _description = "CNC加工" + + workorder_id = fields.Many2one('mrp.workorder', string="工单") + + +class Tray(models.Model): + _inherit = 'sf.tray' + _description = '托盘' + qr_image = fields.Binary(string="托盘二维码", compute='compute_qr_image') + + production_id = fields.Many2one('mrp.production', string='制造订单', + related='workorder_id.production_id' + ) + workorder_id = fields.Many2one('mrp.workorder', string="工单" + ) + + @api.onchange('production_id') + def updateTrayState(self): + + if self.workorder_id != False: + self.state = '占用' + else: + self.state = '空闲' + + def unclamp(self): + self.workorder_id = False + self.production_id = False + self.state = '空闲' + + @api.depends('code') + def compute_qr_image(self): + for item in self: + if not item.code: + item.qr_image = False + continue + # 根据code动态生成二维码图片 + # qr = qrcode.QRCode( + # version=1, + # error_correction=qrcode.constants.ERROR_CORRECT_L, + # box_size=10, + # border=4, + # ) + # qr.add_data(item.code) + # qr.make(fit=True) + # img = qr.make_image() + # 生成条形码文件 + # bar = barcode.get("ean13", "123456789102", writer=ImageWriter()) + # a = bar.get_fullcode() + # b = bar.save('occ') + # 生成条形码图片 + partner_encoder = Code128Encoder(item.code) + # 转换bytes流 + temp = BytesIO() + partner_encoder.save(temp) + # img.save(temp, format='PNG') + qr_image = base64.b64encode(temp.getvalue()) + item.qr_image = qr_image + + +''' +工单绑定托盘信息 +''' + + +class MrpWorkOrder(models.Model): + _inherit = 'mrp.workorder' + _description = '工单' + + tray_ids = fields.One2many('sf.tray', 'workorder_id', string='托盘') + # def get_tray_info(self): + # @api.onchange('X_axis', 'Y_axis', 'Z_axis') + # def get_center_point(self): + # return 'X:%s,Y:%s,Z:%s' % (self.X_axis, self.Y_axis, self.Z_axis) + # 加工面 + # surface = fields.Selection([("前面", "前面"), ("后面", "后面"), ("左面", "左面"), ("右面", "右面"), + # ("上面", "上面")], string="加工面1") + + material_center_point = fields.Char(string='配料中心点') + X1_axis = fields.Float(string='Lx1', default=0) + Y1_axis = fields.Float(string='Ly1', default=0) + Z1_axis = fields.Float(string='Lz1', default=0) + X2_axis = fields.Float(string='Lx2', default=0) + Y2_axis = fields.Float(string='Ly2', default=0) + Z2_axis = fields.Float(string='Lz2', default=0) + X3_axis = fields.Float(string='Fx3', default=0) + Y3_axis = fields.Float(string='Fy3', default=0) + Z3_axis = fields.Float(string='Fz3', default=0) + X4_axis = fields.Float(string='Fx4', default=0) + Y4_axis = fields.Float(string='Fy4', default=0) + Z4_axis = fields.Float(string='Fz4', default=0) + X5_axis = fields.Float(string='Rx5', default=0) + Y5_axis = fields.Float(string='Ry5', default=0) + Z5_axis = fields.Float(string='Rz5', default=0) + X6_axis = fields.Float(string='Rx6', default=0) + Y6_axis = fields.Float(string='Ry6', default=0) + Z6_axis = fields.Float(string='Rz6', default=0) + X7_axis = fields.Float(string='Bx7', default=0) + Y7_axis = fields.Float(string='By7', default=0) + Z7_axis = fields.Float(string='Bz7', default=0) + X8_axis = fields.Float(string='Bx8', default=0) + Y8_axis = fields.Float(string='By8', default=0) + Z8_axis = fields.Float(string='Bz8', default=0) + X9_axis = fields.Float(string='Uz9', default=0) + Y9_axis = fields.Float(string='Uz9', default=0) + Z9_axis = fields.Float(string='Uz9', default=0) + X10_axis = fields.Float(string='Uz10', default=0) + Y10_axis = fields.Float(string='Uz10', default=0) + Z10_axis = fields.Float(string='Uz10', default=0) + + # 计算配料中心点和与x轴倾斜度方法 + def getcenter(self): + x1 = self.X1_axis + x2 = self.X2_axis + x3 = self.X3_axis + x4 = self.X4_axis + x5 = self.X5_axis + x6 = self.X6_axis + x7 = self.X7_axis + x8 = self.X8_axis + y1 = self.Y1_axis + y2 = self.Y2_axis + y3 = self.Y3_axis + y4 = self.Y4_axis + y5 = self.Y5_axis + y6 = self.Y6_axis + y7 = self.Y7_axis + y8 = self.Y8_axis + z1 = self.Z9_axis + x0 = ((x3 - x4) * (x2 * y1 - x1 * y2) - (x1 - x2) * (x4 * y3 - x3 * y4)) / ( + (x3 - x4) * (y1 - y2) - (x1 - x2) * (y3 - y4)) + y0 = ((y3 - y4) * (y2 * x1 - y1 * x2) - (y1 - y2) * (y4 * x3 - y3 * x4)) / ( + (y3 - y4) * (x1 - x2) - (y1 - y2) * (x3 - x4)) + x1 = ((x7 - x8) * (x6 * y5 - x5 * y7) - (x5 - x6) * (x8 * y7 - x7 * y8)) / ( + (x7 - x8) * (y5 - y6) - (x5 - x6) * (y7 - y8)); + y1 = ((y7 - y8) * (y6 * x5 - y5 * x7) - (y5 - y6) * (y8 * x7 - y7 * x8)) / ( + (y7 - y8) * (x5 - x6) - (y5 - y6) * (x7 - x8)) + x = (x0 + x1) / 2 + y = (y0 + y1) / 2 + z = z1 / 2 + + jd = math.atan2((x7 - x8), (y7 - y8)) + jdz = jd * 180 / math.pi + print("(%s,%s)" % (x, y)) + self.material_center_point = ("(%s,%s,%s)" % (x, y, z)) + self.X_deviation_angle = jdz + + X_deviation_angle = fields.Integer(string="X轴偏差度", default=0) + + test_results = fields.Selection([("合格", "合格"), ("返工", "返工"), ("报废", "报废")], string="检测结果") + + cnc_ids = fields.One2many("cnc.processing", 'workorder_id', string="CNC加工") + + tray_code = fields.Char(string="托盘") + + # 扫码绑定托盘方法 + def gettray(self): + if self.tray_code != False: + values = self.env['sf.tray'].search([("code", "=", self.tray_code)]) + if values: + if values.state == "占用": + raise ValidationError('该托盘已占用') + if values.state == "报损": + raise ValidationError('该托盘已损坏') + else: + values.update({ + 'workorder_id': self, + 'production_id': self.production_id, + 'state': '占用', + }) + else: + raise ValidationError('该托盘编码已失效') + else:return "" + + + # 解除托盘绑定 + def unbindtray(self): + tray = self.env['sf.tray'].search([("production_id", "=", self.production_id.id)]) + if tray: + tray.unclamp() + + return "" + + def recreateManufacturing(self): + """ + 重新生成制造订单 + """ + 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_finished_values()) + productions._create_workorder() + 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 in productions: + origin_production = production.move_dest_ids and production.move_dest_ids[ + 0].raw_material_production_id or False + orderpoint = production.orderpoint_id + if orderpoint and orderpoint.create_uid.id == SUPERUSER_ID and orderpoint.trigger == 'manual': + production.message_post( + body=_('This production order has been created from Replenishment Report.'), + message_type='comment', + subtype_xmlid='mail.mt_note') + elif orderpoint: + production.message_post_with_view( + 'mail.message_origin_link', + values={'self': production, 'origin': orderpoint}, + subtype_id=self.env.ref('mail.mt_note').id) + elif origin_production: + production.message_post_with_view( + 'mail.message_origin_link', + values={'self': production, 'origin': origin_production}, + subtype_id=self.env.ref('mail.mt_note').id) + + # print(productions) + return "" + + def recreateWorkerOrder(self): + """ + 返工重新生成工单 + """ + 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_workorder1(self.processing_panel) + return "" + + def fetchCNC(self): + return "" + + +''' +制造订单绑定托盘信息 +''' + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + _description = "制造订单" + tray_ids = fields.One2many('sf.tray', 'production_id', string="托盘") + + def create_production1_values(self, production): + production_values_str = {'origin': production.origin, + 'product_id': production.product_id.id, + 'product_description_variants': production.product_description_variants, + 'product_qty': production.product_qty, + 'product_uom_id': production.product_uom_id.id, + 'location_src_id': production.location_src_id.id, + 'location_dest_id': production.location_dest_id.id, + 'bom_id': production.bom_id.id, + 'date_deadline': production.date_deadline, + 'date_planned_start': production.date_planned_start, + 'date_planned_finished': production.date_planned_finished, + 'procurement_group_id': False, + 'propagate_cancel': production.propagate_cancel, + 'orderpoint_id': production.orderpoint_id.id, + 'picking_type_id': production.picking_type_id.id, + 'company_id': production.company_id.id, + 'move_dest_ids': production.move_dest_ids.ids, + 'user_id': production.user_id.id} + return production_values_str + + def create_workorder1(self, k): + for production in self: + if not production.bom_id or not production.product_id: + continue + workorders_values = [] + + product_qty = production.product_uom_id._compute_quantity(production.product_qty, + production.bom_id.product_uom_id) + exploded_boms, dummy = production.bom_id.explode(production.product_id, + product_qty / production.bom_id.product_qty, + picking_type=production.bom_id.picking_type_id) + + for bom, bom_data in exploded_boms: + # If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders. + if not (bom.operation_ids and (not bom_data['parent_line'] or bom_data[ + 'parent_line'].bom_id.operation_ids != bom.operation_ids)): + continue + for operation in bom.operation_ids: + if operation._skip_operation_line(bom_data['product']): + continue + workorders_values += [{ + 'name': operation.name, + 'production_id': production.id, + 'workcenter_id': operation.workcenter_id.id, + 'product_uom_id': production.product_uom_id.id, + 'operation_id': operation.id, + 'state': 'pending', + }] + # 根据加工面板的面数及对应的工序模板生成工单 + i = 0 + production.product_id.model_processing_panel = k + processing_panel_len = len(k) + for k in (production.product_id.model_processing_panel.split(',')): + routingworkcenter = self.env['sf.model.type.routing.sort'].search( + [('model_type_id', '=', production.product_id.model_type_id.id)], + + order='sequence asc' + ) + i += 1 + for route in routingworkcenter: + + if route.routing_type == 'CNC加工': + workorders_values.append( + self.env['mrp.workorder'].json_workorder_str(k, production, route)) + if route.routing_type == '后置三元质量检测': + workorders_values.append( + self.env['mrp.workorder'].json_workorder_str(k, production, route)) + + production.workorder_ids = workorders_values + for workorder in production.workorder_ids: + workorder.duration_expected = workorder._get_duration_expected() + + +class Attachment(models.Model): + _inherit = 'ir.attachment' + + cnc_model = fields.Binary('cnc文件', attachment=False) + model_name = fields.Char('模型名称') diff --git a/sf_route_workcenter/report/sf_tray_report.xml b/sf_route_workcenter/report/sf_tray_report.xml index d1af6676..f0bcc642 100644 --- a/sf_route_workcenter/report/sf_tray_report.xml +++ b/sf_route_workcenter/report/sf_tray_report.xml @@ -1,6 +1,6 @@ - + Dymo Label Sheet @@ -16,6 +16,7 @@ 96 + 打印条形码 sf.tray @@ -27,7 +28,7 @@ - +