diff --git a/jikimo_work_reporting_api/__init__.py b/jikimo_work_reporting_api/__init__.py new file mode 100644 index 00000000..9e5827f9 --- /dev/null +++ b/jikimo_work_reporting_api/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import controllers +from . import models diff --git a/jikimo_work_reporting_api/__manifest__.py b/jikimo_work_reporting_api/__manifest__.py new file mode 100644 index 00000000..502ce9d5 --- /dev/null +++ b/jikimo_work_reporting_api/__manifest__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +{ + 'name': '机企猫 报工系统API', + 'version': '1.0.0', + 'summary': """ 机企猫 报工系统API """, + 'author': '机企猫', + 'website': 'https://xt.sf.jikimo.com', + 'category': 'sf', + 'depends': ['base', 'sf_maintenance', 'jikimo_mini_program'], + 'data': [ + + ], + + 'application': True, + 'installable': True, + 'auto_install': False, + 'license': 'LGPL-3', +} diff --git a/jikimo_work_reporting_api/controllers/__init__.py b/jikimo_work_reporting_api/controllers/__init__.py new file mode 100644 index 00000000..757b12a1 --- /dev/null +++ b/jikimo_work_reporting_api/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/jikimo_work_reporting_api/controllers/main.py b/jikimo_work_reporting_api/controllers/main.py new file mode 100644 index 00000000..76b55dc9 --- /dev/null +++ b/jikimo_work_reporting_api/controllers/main.py @@ -0,0 +1,42 @@ +import json +from odoo import http +from odoo.http import request +from odoo.addons.sf_machine_connect.models.ftp_operate import transfer_nc_files + +class MainController(http.Controller): + + @http.route('/api/manual_download_program', type='json', methods=['POST'], auth='wechat_token', cors='*') + def manual_download_program(self): + """ + 人工线下加工传输编程文件 + """ + data = json.loads(request.httprequest.data) + maintenance_equipment_name = data.get('maintenance_equipment_name') + model_id = data.get('model_id') + if not maintenance_equipment_name or not model_id: + return {'code': 400, 'message': '参数错误'} + maintenance_equipment = request.env['maintenance.equipment'].sudo().search([('name', '=', maintenance_equipment_name)], limit=1) + if not maintenance_equipment: + return {'code': 400, 'message': '机床不存在'} + ftp_resconfig = request.env['res.config.settings'].sudo().get_values() + source_ftp_info = { + 'host': ftp_resconfig['ftp_host'], + 'port': int(ftp_resconfig['ftp_port']), + 'username': ftp_resconfig['ftp_user'], + 'password': ftp_resconfig['ftp_password'] + } + target_ftp_info = { + 'host': maintenance_equipment.ftp_host, + 'port': int(maintenance_equipment.ftp_port), + 'username': maintenance_equipment.ftp_username, + 'password': maintenance_equipment.ftp_password + } + # 传输nc文件 + if transfer_nc_files( + source_ftp_info, + target_ftp_info, + '/' + str(model_id), + '/home/jikimo/testdir'): + return {'code': 200, 'message': 'success'} + else: + return {'code': 500, 'message': '传输失败'} diff --git a/jikimo_work_reporting_api/models/__init__.py b/jikimo_work_reporting_api/models/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/jikimo_work_reporting_api/models/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py b/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py index 0ab0d6ae..f4a49c1f 100644 --- a/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py +++ b/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py @@ -52,10 +52,10 @@ class JikimoWorkorderException(models.Model): def _get_message(self, message_queue_ids): contents, _ = super(JikimoWorkorderException, self)._get_message(message_queue_ids) - url = self.env['ir.config_parameter'].get_param('web.base.url') + base_url = self.env['ir.config_parameter'].get_param('web.base.url') action_id = self.env.ref('mrp.mrp_production_action').id for index, content in enumerate(contents): exception_id = self.env['jikimo.workorder.exception'].browse(message_queue_ids[index].res_id) - url = url + '/web#id=%s&view_type=form&action=%s' % (exception_id.workorder_id.production_id.id, action_id) + url = base_url + '/web#id=%s&view_type=form&action=%s' % (exception_id.workorder_id.production_id.id, action_id) contents[index] = content.replace('{{url}}', url) return contents, message_queue_ids diff --git a/quality_control/models/quality.py b/quality_control/models/quality.py index 47b21dc7..9be466f5 100644 --- a/quality_control/models/quality.py +++ b/quality_control/models/quality.py @@ -206,7 +206,7 @@ class QualityCheck(models.Model): ('NG', 'NG') ], string='出厂检验报告结果', default='OK') measure_operator = fields.Many2one('res.users', string='操机员') - quality_manager = fields.Many2one('res.users', string='质检员', compute='_compute_quality_manager', store=True) + quality_manager = fields.Many2one('res.users', string='质检员', compute='_compute_quality_manager') @api.depends('measure_line_ids') def _compute_quality_manager(self): diff --git a/sf_base/commons/Printer.py b/sf_base/commons/Printer.py index 563d9dea..e9d31282 100644 --- a/sf_base/commons/Printer.py +++ b/sf_base/commons/Printer.py @@ -8,6 +8,7 @@ class Printer(models.Model): name = fields.Char(string='名称', required=True) ip_address = fields.Char(string='IP 地址', required=True) port = fields.Integer(string='端口', default=9100) + type = fields.Selection([('zpl', 'ZPL'), ('normal', '普通')], string='类型', default='zpl') class TableStyle(models.Model): diff --git a/sf_base/commons/common.py b/sf_base/commons/common.py index 9f359c9c..27b56038 100644 --- a/sf_base/commons/common.py +++ b/sf_base/commons/common.py @@ -2,7 +2,16 @@ import time, datetime import hashlib from odoo import models +from typing import Optional import socket +import os +import logging +import qrcode +from reportlab.pdfgen import canvas +from reportlab.lib.units import inch +from PyPDF2 import PdfFileReader, PdfFileWriter +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont class Common(models.Model): _name = 'sf.sync.common' @@ -92,3 +101,120 @@ class PrintingUtils(models.AbstractModel): # host = "192.168.50.110" # 可以作为参数传入,或者在此配置 # port = 9100 # 可以作为参数传入,或者在此配置 self.send_to_printer(host, port, zpl_code) + + + def add_qr_code_to_pdf(self, pdf_path:str, content:str, buttom_text:Optional[str]=False): + """ + 在PDF文件中添加二维码 + :param pdf_path: PDF文件路径 + :param content: 二维码内容 + :param buttom_text: 二维码下方文字 + :return: 是否成功 + """ + if not os.path.exists(pdf_path): + logging.warning(f'PDF文件不存在: {pdf_path}') + return False + + # 生成二维码 + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(str(content)) + qr.make(fit=True) + qr_img = qr.make_image(fill_color="black", back_color="white") + + # 保存二维码为临时文件 + qr_temp_path = '/tmp/qr_temp.png' + qr_img.save(qr_temp_path) + + # 创建一个临时PDF文件路径 + output_temp_path = '/tmp/output_temp.pdf' + + try: + # 使用reportlab创建一个新的PDF + + + # 注册中文字体 + font_paths = [ + "/usr/share/fonts/windows/simsun.ttc", # Windows系统宋体 + "c:/windows/fonts/simsun.ttc", # Windows系统宋体另一个位置 + "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", # Linux Droid字体 + "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", # 文泉驿正黑 + "/usr/share/fonts/chinese/TrueType/simsun.ttc", # 某些Linux发行版位置 + ] + + font_found = False + for font_path in font_paths: + if os.path.exists(font_path): + try: + pdfmetrics.registerFont(TTFont('SimSun', font_path)) + font_found = True + break + except: + continue + + # 读取原始PDF + with open(pdf_path, "rb") as original_file: + existing_pdf = PdfFileReader(original_file) + output = PdfFileWriter() + + # 处理第一页 + page = existing_pdf.getPage(0) + # 获取页面尺寸 + page_width = float(page.mediaBox.getWidth()) + page_height = float(page.mediaBox.getHeight()) + + # 创建一个新的PDF页面用于放置二维码 + c = canvas.Canvas(output_temp_path, pagesize=(page_width, page_height)) + + # 设置字体 + if font_found: + c.setFont('SimSun', 14) # 增大字体大小到14pt + else: + # 如果没有找到中文字体,使用默认字体 + c.setFont('Helvetica', 14) + logging.warning("未找到中文字体,将使用默认字体") + + # 在右下角绘制二维码,预留边距 + qr_size = 1.5 * inch # 二维码大小为2英寸 + margin = 0.1 * inch # 边距为0.4英寸 + qr_y = margin + 20 # 将二维码向上移动一点,为文字留出空间 + c.drawImage(qr_temp_path, page_width - qr_size - margin, qr_y, width=qr_size, height=qr_size) + + if buttom_text: + # 在二维码下方绘制文字 + text = buttom_text + text_width = c.stringWidth(text, "SimSun" if font_found else "Helvetica", 14) # 准确计算文字宽度 + text_x = page_width - qr_size - margin + (qr_size - text_width) / 2 # 文字居中对齐 + text_y = margin + 20 # 文字位置靠近底部 + c.drawString(text_x, text_y, text) + + c.save() + + # 读取带有二维码的临时PDF + with open(output_temp_path, "rb") as qr_file: + qr_pdf = PdfFileReader(qr_file) + qr_page = qr_pdf.getPage(0) + + # 合并原始页面和二维码页面 + page.mergePage(qr_page) + output.addPage(page) + + # 添加剩余的页面 + for i in range(1, existing_pdf.getNumPages()): + output.addPage(existing_pdf.getPage(i)) + + # 保存最终的PDF到一个临时文件 + final_temp_path = pdf_path + '.tmp' + with open(final_temp_path, "wb") as output_file: + output.write(output_file) + + # 替换原始文件 + os.replace(final_temp_path, pdf_path) + + return True + + finally: + # 清理临时文件 + if os.path.exists(qr_temp_path): + os.remove(qr_temp_path) + if os.path.exists(output_temp_path): + os.remove(output_temp_path) \ No newline at end of file diff --git a/sf_base/views/Printer.xml b/sf_base/views/Printer.xml index 0e199b32..157121c9 100644 --- a/sf_base/views/Printer.xml +++ b/sf_base/views/Printer.xml @@ -9,6 +9,7 @@ + @@ -24,6 +25,7 @@ + diff --git a/sf_dlm/data/product_data.xml b/sf_dlm/data/product_data.xml index b8cab582..06bed6ea 100644 --- a/sf_dlm/data/product_data.xml +++ b/sf_dlm/data/product_data.xml @@ -14,10 +14,12 @@ 原材料 原材料 - 表面工艺 表面工艺 + + fifo + manual_periodic @@ -40,10 +42,10 @@ - - - - + + + + 功能刀具 diff --git a/sf_dlm_management/__init__.py b/sf_dlm_management/__init__.py index e69de29b..9a7e03ed 100644 --- a/sf_dlm_management/__init__.py +++ b/sf_dlm_management/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/sf_dlm_management/__manifest__.py b/sf_dlm_management/__manifest__.py index 9a94082a..96344803 100644 --- a/sf_dlm_management/__manifest__.py +++ b/sf_dlm_management/__manifest__.py @@ -9,8 +9,9 @@ """, 'category': 'sf', 'website': 'https://www.sf.jikimo.com', - 'depends': ['sf_sale', 'sf_dlm', 'sf_manufacturing','jikimo_attachment_viewer'], + 'depends': ['sf_sale', 'sf_dlm', 'sf_manufacturing', 'jikimo_attachment_viewer'], 'data': [ + 'data/sequence.xml', 'data/stock_data.xml', 'views/product_template_management_view.xml', ], diff --git a/sf_dlm_management/data/sequence.xml b/sf_dlm_management/data/sequence.xml new file mode 100644 index 00000000..39379169 --- /dev/null +++ b/sf_dlm_management/data/sequence.xml @@ -0,0 +1,10 @@ + + + + 工艺可选参数编码序列 + sf.production.process.parameter + WKSP + 9 + + + \ No newline at end of file diff --git a/sf_dlm_management/models/__init__.py b/sf_dlm_management/models/__init__.py index 8c38257e..a46a54df 100644 --- a/sf_dlm_management/models/__init__.py +++ b/sf_dlm_management/models/__init__.py @@ -1,2 +1,4 @@ # from . import product_template # from . import product_supplierinfo +from . import sf_production_common +from . import mrp_routing_workcenter \ No newline at end of file diff --git a/sf_dlm_management/models/mrp_routing_workcenter.py b/sf_dlm_management/models/mrp_routing_workcenter.py new file mode 100644 index 00000000..4a04eb87 --- /dev/null +++ b/sf_dlm_management/models/mrp_routing_workcenter.py @@ -0,0 +1,16 @@ +import logging +from odoo import fields, models, api +from odoo.exceptions import UserError +from odoo.tools import str2bool + + +class ResMrpRoutingWorkcenter(models.Model): + _inherit = 'mrp.routing.workcenter' + def init(self): + super(ResMrpRoutingWorkcenter, self).init() + # 在模块初始化时触发计算字段的更新 + records = self.search([]) + if str2bool(self.env['ir.config_parameter'].get_param('sf.production.process.parameter.is_init_workcenter',default='False')): + return + records.optional_process_parameters_date() + self.env['ir.config_parameter'].set_param('sf.production.process.parameter.is_init_workcenter', True) \ No newline at end of file diff --git a/sf_dlm_management/models/sf_production_common.py b/sf_dlm_management/models/sf_production_common.py new file mode 100644 index 00000000..cd19e938 --- /dev/null +++ b/sf_dlm_management/models/sf_production_common.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +import logging +from odoo import fields, models, api +from odoo.exceptions import UserError +from odoo.tools import str2bool + + +class SfProductionProcessParameter(models.Model): + _inherit = 'sf.production.process.parameter' + + + @api.model + def create(self, vals): + if vals.get('code', '/') == '/' or vals.get('code', '/') is False: + vals['code'] = self.env['ir.sequence'].next_by_code('sf.production.process.parameter') or '/' + if not vals.get('process_id') and vals.get('routing_id'): + vals['gain_way'] = '外协' + routing_id = self.env['mrp.routing.workcenter'].browse(vals.get('routing_id')) + if routing_id.surface_technics_id: + vals['process_id'] = routing_id.surface_technics_id.id + obj = super(SfProductionProcessParameter, self).create(vals) + return obj + def create_service_product(self): + service_categ = self.env.ref( + 'sf_dlm.product_category_surface_technics_sf').sudo() + + product_name = f"{self.process_id.name}{self.name}" + product_id = self.env['product.template'].search( + [("name", '=', product_name)]) + if product_id: + product_id.server_product_process_parameters_id = self.id + else: + self.env['product.template'].create({ + 'detailed_type': 'service', + 'name': product_name, + 'invoice_policy': 'delivery', + 'categ_id': service_categ.id, + 'description': f"基于{self.name}创建的服务产品", + 'sale_ok': True, # 可销售 + 'purchase_ok': True, # 可采购 + 'server_product_process_parameters_id': self.id, + }) + + def create_work_center(self): + production_process_parameter = self + if not production_process_parameter.process_id: + return + if not production_process_parameter.routing_id: + workcenter_id = self.env['mrp.routing.workcenter'].search( + [("surface_technics_id", '=', production_process_parameter.process_id.id)]) + if not workcenter_id: + outsourcing_work_center = self.env['mrp.workcenter'].search( + [("name", '=', '外协工作中心')]) + routing_id = self.env['mrp.routing.workcenter'].create({ + 'workcenter_ids': [(6, 0, outsourcing_work_center.ids)], + 'routing_tag': 'special', + 'routing_type': '表面工艺', + 'is_outsource': True, + 'surface_technics_id': production_process_parameter.process_id.id, + 'name': production_process_parameter.process_id.name, + }) + production_process_parameter.routing_id = routing_id.id + else: + production_process_parameter.routing_id = workcenter_id.id + + def init(self): + super(SfProductionProcessParameter, self).init() + # 在模块初始化时触发计算字段的更新 + records = self.search([]) + if str2bool(self.env['ir.config_parameter'].get_param('sf.production.process.parameter.is_init_process', + default='False')): + return + for record in records: + if not record.outsourced_service_products: + record.create_service_product() + record.create_work_center() + self.env['ir.config_parameter'].set_param('sf.production.process.parameter.is_init_process', True) diff --git a/sf_dlm_management/views/product_template_management_view.xml b/sf_dlm_management/views/product_template_management_view.xml index 01c2f51b..d6420417 100644 --- a/sf_dlm_management/views/product_template_management_view.xml +++ b/sf_dlm_management/views/product_template_management_view.xml @@ -39,7 +39,7 @@ attrs="{'invisible': [('categ_type', 'not in', ['成品','坯料', '原材料'])],'readonly': [('id', '!=', False)]}"/> - + + + + + + + + + + + + + + + diff --git a/sf_manufacturing/__manifest__.py b/sf_manufacturing/__manifest__.py index ba0c6751..24c8a4b0 100644 --- a/sf_manufacturing/__manifest__.py +++ b/sf_manufacturing/__manifest__.py @@ -10,7 +10,8 @@ """, 'category': 'sf', 'website': 'https://www.sf.jikimo.com', - 'depends': ['sf_base', 'sf_maintenance', 'web_widget_model_viewer', 'sf_warehouse','jikimo_attachment_viewer', 'jikimo_sale_multiple_supply_methods'], + 'depends': ['sf_base', 'sf_maintenance', 'web_widget_model_viewer', 'sf_warehouse', 'jikimo_attachment_viewer', + 'jikimo_sale_multiple_supply_methods', 'product'], 'data': [ 'data/cron_data.xml', 'data/stock_data.xml', @@ -18,6 +19,7 @@ 'data/panel_data.xml', 'data/sf_work_individuation_page.xml', 'data/agv_scheduling_data.xml', + 'data/product_data.xml', 'security/group_security.xml', 'security/ir.model.access.csv', 'wizard/workpiece_delivery_views.xml', @@ -28,6 +30,7 @@ 'wizard/mrp_workorder_batch_replan_wizard_views.xml', 'wizard/sf_programming_reason_views.xml', 'wizard/sale_order_cancel_views.xml', + 'wizard/process_outsourcing.xml', 'views/mrp_views_menus.xml', 'views/agv_scheduling_views.xml', 'views/stock_lot_views.xml', @@ -44,6 +47,7 @@ 'views/sale_order_views.xml', 'views/mrp_workorder_batch_replan.xml', 'views/purchase_order_view.xml', + 'views/product_template_views.xml', ], 'assets': { diff --git a/sf_manufacturing/data/product_data.xml b/sf_manufacturing/data/product_data.xml new file mode 100644 index 00000000..8331c373 --- /dev/null +++ b/sf_manufacturing/data/product_data.xml @@ -0,0 +1,23 @@ + + + + + 服务 + + fifo + manual_periodic + + + 工序外协 + + fifo + manual_periodic + + + 其他 + + fifo + manual_periodic + + + \ No newline at end of file diff --git a/sf_manufacturing/models/__init__.py b/sf_manufacturing/models/__init__.py index e6845317..9bad61c6 100644 --- a/sf_manufacturing/models/__init__.py +++ b/sf_manufacturing/models/__init__.py @@ -16,4 +16,6 @@ from . import sf_production_common from . import sale_order from . import quick_easy_order from . import purchase_order -from . import quality_check \ No newline at end of file +from . import quality_check +from . import purchase_request_line +from . import workorder_printer diff --git a/sf_manufacturing/models/mrp_production.py b/sf_manufacturing/models/mrp_production.py index 52c5f463..02125aca 100644 --- a/sf_manufacturing/models/mrp_production.py +++ b/sf_manufacturing/models/mrp_production.py @@ -6,6 +6,7 @@ import json import os import re import traceback +from operator import itemgetter import requests from itertools import groupby @@ -238,7 +239,8 @@ class MrpProduction(models.Model): programming_no = fields.Char('编程单号') work_state = fields.Char('业务状态') programming_state = fields.Selection( - [('编程中', '编程中'), ('已编程', '已编程'), ('已编程未下发', '已编程未下发'), ('已下发', '已下发'), ('已取消', '已取消')], + [('编程中', '编程中'), ('已编程', '已编程'), ('已编程未下发', '已编程未下发'), ('已下发', '已下发'), + ('已取消', '已取消')], string='编程状态', tracking=True) glb_file = fields.Binary("glb模型文件") @@ -267,6 +269,7 @@ class MrpProduction(models.Model): quality_standard = fields.Binary('质检标准', related='product_id.quality_standard', readonly=True) part_name = fields.Char(string='零件名称', compute='_compute_part_info', store=True) + @api.depends('product_id') def _compute_part_info(self): try: @@ -400,8 +403,10 @@ class MrpProduction(models.Model): and production.schedule_state == '已排' and production.is_rework is False): production.state = 'pending_cam' if any((wo.test_results == '返工' and wo.state == 'done' and - (production.programming_state in ['已编程'] or(wo.individuation_page_list and 'PTD' in wo.individuation_page_list))) - or (wo.is_rework is True and wo.state == 'done' and production.programming_state in ['编程中', '已编程']) + (production.programming_state in ['已编程'] or ( + wo.individuation_page_list and 'PTD' in wo.individuation_page_list))) + or (wo.is_rework is True and wo.state == 'done' and production.programming_state in ['编程中', + '已编程']) for wo in production.workorder_ids) or production.is_rework is True: production.state = 'rework' if any(wo.test_results == '报废' and wo.state == 'done' for wo in production.workorder_ids): @@ -617,7 +622,7 @@ class MrpProduction(models.Model): for rp in reproduction: if rp.programming_no == item['programming_no']: rp.write({'programming_state': '已编程未下发' if item[ - 'programming_state'] == '已编程' else '编程中'}) + 'programming_state'] == '已编程' else '编程中'}) else: return item @@ -889,11 +894,44 @@ class MrpProduction(models.Model): workorders_values.append( self.env[ 'mrp.workorder']._json_workorder_surface_process_str( - production, route, product_production_process.seller_ids[0].partner_id.id)) + production, route, product_production_process.seller_ids[ + 0].partner_id.id if product_production_process.seller_ids else False)) production.workorder_ids = workorders_values for workorder in production.workorder_ids: workorder.duration_expected = workorder._get_duration_expected() + def _create_subcontract_purchase_request(self, purchase_request_line): + sorted_list = sorted(purchase_request_line, key=itemgetter('name')) + grouped_purchase_request_line = { + k: list(g) + for k, g in groupby(sorted_list, key=itemgetter('name')) + } + for name, request_line in grouped_purchase_request_line.items(): + request_line_sorted_list = sorted(request_line, key=itemgetter('product_id')) + grouped_purchase_request_line_sorted_list = { + k: list(g) + for k, g in groupby(request_line_sorted_list, key=itemgetter('product_id')) + } + purchase_request_model = self.env["purchase.request"] + origin = ", ".join({item['production_name'] for item in request_line_sorted_list if item.get('production_name')}) + pr = purchase_request_model.create({ + "origin": origin, + "company_id": self.company_id.id, + "picking_type_id": self.env.ref('stock.picking_type_in').id, + "group_id": request_line[0].get('group_id'), + "requested_by": self.env.context.get("uid", self.env.uid), + "assigned_to": False, + "bom_id": self[0].bom_id.id, + }) + for product_id, request_line_list in grouped_purchase_request_line_sorted_list.items(): + cur_request_line = request_line_list[0] + cur_request_line['product_qty'] = len(request_line_list) + cur_request_line['request_id'] = pr.id + cur_request_line['origin'] = ", ".join({item['production_name'] for item in request_line_list if item.get('production_name')}) + cur_request_line.pop('group_id', None) + cur_request_line.pop('production_name', None) + self.env["purchase.request.line"].create(cur_request_line) + # 外协出入库单处理 def get_subcontract_pick_purchase(self): production_all = self.sorted(lambda x: x.id) @@ -903,6 +941,7 @@ class MrpProduction(models.Model): for product_id, pd in grouped_product_ids.items(): product_id_to_production_names[product_id] = [p.name for p in pd] sorted_workorders = None + purchase_request_line = [] for production in production_all: proc_workorders = [] process_parameter_workorder = self.env['mrp.workorder'].search( @@ -919,7 +958,10 @@ class MrpProduction(models.Model): return for workorders in reversed(sorted_workorders): self.env['stock.picking'].create_outcontract_picking(workorders, production, sorted_workorders) - self.env['purchase.order'].get_purchase_order(workorders, production, product_id_to_production_names) + # self.env['purchase.order'].get_purchase_order(workorders, production, product_id_to_production_names) + purchase_request_line = purchase_request_line + self.env['purchase.order'].get_purchase_request( + workorders, production) + self._create_subcontract_purchase_request(purchase_request_line) # 工单排序 def _reset_work_order_sequence1(self, k): @@ -1341,7 +1383,7 @@ class MrpProduction(models.Model): 'default_reprogramming_num': cloud_programming.get('reprogramming_num') if cloud_programming else '', 'default_programming_state': cloud_programming.get('programming_state') if cloud_programming else '', 'default_is_reprogramming': True if cloud_programming and ( - cloud_programming.get('programming_state') in ['已下发']) else False + cloud_programming.get('programming_state') in ['已下发']) else False } } @@ -1728,7 +1770,8 @@ class MrpProduction(models.Model): raise UserError('仅支持选择单个制造订单进行编程申请,请重新选择') for production in self: if production.state not in ['confirmed', 'pending_cam'] or production.programming_state != '已编程': - raise UserError('不可操作。所选制造订单必须同时满足如下条件:\n1、制造订单状态:待排程 或 待加工;\n2、制造订单编程状态:已编程。\n请检查!') + raise UserError( + '不可操作。所选制造订单必须同时满足如下条件:\n1、制造订单状态:待排程 或 待加工;\n2、制造订单编程状态:已编程。\n请检查!') cloud_programming = production._cron_get_programming_state() if cloud_programming['programming_state'] in ['待编程', '已编程', '编程中']: raise UserError("当前编程单正在重新编程,请注意查看当前制造订单的“编程记录”确认进度!") diff --git a/sf_manufacturing/models/mrp_routing_workcenter.py b/sf_manufacturing/models/mrp_routing_workcenter.py index 666be375..c869f01f 100644 --- a/sf_manufacturing/models/mrp_routing_workcenter.py +++ b/sf_manufacturing/models/mrp_routing_workcenter.py @@ -1,6 +1,7 @@ import logging from odoo import fields, models, api from odoo.exceptions import UserError +from odoo.tools import str2bool class ResMrpRoutingWorkcenter(models.Model): @@ -24,10 +25,41 @@ class ResMrpRoutingWorkcenter(models.Model): workcenter_ids = fields.Many2many('mrp.workcenter', 'rel_workcenter_route', required=True) bom_id = fields.Many2one('mrp.bom', required=False) surface_technics_id = fields.Many2one('sf.production.process', string="表面工艺") + optional_process_parameters = fields.One2many('sf.production.process.parameter','routing_id',string='可选工艺参数') reserved_duration = fields.Float('预留时长', default=30, tracking=True) is_outsource = fields.Boolean('外协', default=False) individuation_page_ids = fields.Many2many('sf.work.individuation.page', string='个性化记录') + @api.onchange('surface_technics_id') + def optional_process_parameters_date(self): + for record in self: + if not record.surface_technics_id: + continue + parameter_ids = self.env['sf.production.process.parameter'].search([ + ('process_id', '=', record.surface_technics_id.id), + ]) + record.optional_process_parameters = parameter_ids.ids + + # @api.model + # def _auto_init(self): + # # 先执行标准初始化 + # res = super(ResMrpRoutingWorkcenter, self)._auto_init() + # # 然后执行自定义初始化 + # records = self.search([]) + # if str2bool(self.env['ir.config_parameter'].get_param('sf.production.process.parameter.is_init_workcenter', + # default='False')): + # return + # records.optional_process_parameters_date() + # self.env['ir.config_parameter'].set_param('sf.production.process.parameter.is_init_workcenter', True) + # return res + # def init(self): + # super(ResMrpRoutingWorkcenter, self).init() + # # 在模块初始化时触发计算字段的更新 + # records = self.search([]) + # if str2bool(self.env['ir.config_parameter'].get_param('sf.production.process.parameter.is_init_workcenter',default='False')): + # return + # records.optional_process_parameters_date() + # self.env['ir.config_parameter'].set_param('sf.production.process.parameter.is_init_workcenter', True) def get_no(self): international_standards = self.search( [('code', '!=', ''), ('active', 'in', [True, False])], diff --git a/sf_manufacturing/models/mrp_workcenter.py b/sf_manufacturing/models/mrp_workcenter.py index 27fdb37c..9434651c 100644 --- a/sf_manufacturing/models/mrp_workcenter.py +++ b/sf_manufacturing/models/mrp_workcenter.py @@ -21,7 +21,16 @@ class ResWorkcenter(models.Model): related='equipment_id.production_line_id', store=True) is_process_outsourcing = fields.Boolean('工艺外协') users_ids = fields.Many2many("res.users", 'users_workcenter', tracking=True) - + @api.constrains('name') + def _check_unique_name_code(self): + for record in self: + # 检查是否已经存在相同的 name 和 code 组合 + existing = self.search([ + ('name', '=', record.name), + ('id', '!=', record.id) # 排除当前记录 + ]) + if existing: + raise ValueError('记录已存在') def write(self, vals): if 'users_ids' in vals: old_users = self.users_ids diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py index f187d506..8d5ecbf1 100644 --- a/sf_manufacturing/models/mrp_workorder.py +++ b/sf_manufacturing/models/mrp_workorder.py @@ -19,7 +19,6 @@ from odoo.addons.sf_mrs_connect.models.ftp_operate import FtpController class ResMrpWorkOrder(models.Model): _inherit = 'mrp.workorder' - _order = 'sequence asc' _description = '工单' product_tmpl_name = fields.Char('坯料产品名称', related='production_bom_id.bom_line_ids.product_id.name') @@ -106,11 +105,11 @@ class ResMrpWorkOrder(models.Model): record.back_button_display = False # tag_type if cur_workorder.is_subcontract or cur_workorder.routing_type == '解除装夹' or cur_workorder.routing_type == '切割' or any( - detection_result.processing_panel == cur_workorder.processing_panel and - detection_result.routing_type == cur_workorder.routing_type and - cur_workorder.tag_type !='重新加工' and - detection_result.test_results != '合格' - for detection_result in cur_workorder.production_id.detection_result_ids + detection_result.processing_panel == cur_workorder.processing_panel and + detection_result.routing_type == cur_workorder.routing_type and + cur_workorder.tag_type != '重新加工' and + detection_result.test_results != '合格' + for detection_result in cur_workorder.production_id.detection_result_ids ): record.back_button_display = False else: @@ -122,11 +121,11 @@ class ResMrpWorkOrder(models.Model): else: record.back_button_display = False if cur_workorder.is_subcontract or cur_workorder.routing_type == '解除装夹' or cur_workorder.routing_type == '切割' or any( - detection_result.processing_panel == cur_workorder.processing_panel and - detection_result.routing_type == cur_workorder.routing_type and - cur_workorder.tag_type !='重新加工' and - detection_result.test_results != '合格' - for detection_result in cur_workorder.production_id.detection_result_ids + detection_result.processing_panel == cur_workorder.processing_panel and + detection_result.routing_type == cur_workorder.routing_type and + cur_workorder.tag_type != '重新加工' and + detection_result.test_results != '合格' + for detection_result in cur_workorder.production_id.detection_result_ids ): record.back_button_display = False @@ -429,6 +428,8 @@ class ResMrpWorkOrder(models.Model): def _compute_surface_technics_purchase_ids(self): for order in self: if order.routing_type == '表面工艺' and order.state not in ['cancel']: + # domain = [('group_id', '=', self.production_id.procurement_group_id.id), + # ('purchase_type', '=', 'consignment'), ('state', '!=', 'cancel')] domain = [('purchase_type', '=', 'consignment'), ('origin', 'like', '%' + self.production_id.name + '%'), ('state', '!=', 'cancel')] @@ -473,13 +474,14 @@ class ResMrpWorkOrder(models.Model): def _get_surface_technics_purchase_ids(self): domain = [('origin', 'like', '%' + self.production_id.name + '%'), ('purchase_type', '=', 'consignment')] - purchase_orders = self.env['purchase.order'].search(domain) + # domain = [('group_id', '=', self.production_id.procurement_group_id.id), ('purchase_type', '=', 'consignment')] + purchase_orders = self.env['purchase.order'].search(domain, order='id desc', # 按创建时间降序(最新的在前) + limit=1) purchase_orders_id = self.env['purchase.order'] for po in purchase_orders: for line in po.order_line: if line.product_id.server_product_process_parameters_id == self.surface_technics_parameters_id: - if line.product_qty == 1: - purchase_orders_id = line.order_id + purchase_orders_id = line.order_id return purchase_orders_id supplier_id = fields.Many2one('res.partner', string='外协供应商') @@ -1200,6 +1202,7 @@ class ResMrpWorkOrder(models.Model): 'cmm_ids': production.workorder_ids.filtered(lambda t: t.routing_type == 'CNC加工').cmm_ids, }] return workorders_values_str + def _process_compute_state(self): for workorder in self: # 如果工单的工序没有进行排序则跳出循环 @@ -1224,7 +1227,7 @@ class ResMrpWorkOrder(models.Model): and workorder.production_id.schedule_state == '已排' and len(workorder.production_id.picking_ids.filtered( lambda w: w.state not in ['done', 'cancel'])) == 0): - # and workorder.production_id.programming_state == '已编程' + # and workorder.production_id.programming_state == '已编程' if workorder.is_subcontract is True: if workorder.production_id.state == 'rework': workorder.state = 'waiting' @@ -1287,6 +1290,7 @@ class ResMrpWorkOrder(models.Model): mo.get_move_line(workorder.production_id, workorder)) else: workorder.state = 'waiting' + @api.depends('production_availability', 'blocked_by_workorder_ids', 'blocked_by_workorder_ids.state', 'production_id.tool_state', 'production_id.schedule_state', 'sequence', 'production_id.programming_state') @@ -1301,6 +1305,7 @@ class ResMrpWorkOrder(models.Model): for check_id in workorder.check_ids: if not check_id.is_inspect: check_id.quality_state = 'none' + # 重写工单开始按钮方法 def button_start(self): # 判断工单状态是否为等待组件 @@ -1534,7 +1539,7 @@ class ResMrpWorkOrder(models.Model): # workorder.rfid_code_old = rfid_code # workorder.rfid_code = False logging.info('workorder.rfid_code:%s' % workorder.rfid_code) - + if is_production_id is True: logging.info('product_qty:%s' % record.production_id.product_qty) for move_raw_id in record.production_id.move_raw_ids: @@ -1552,7 +1557,8 @@ class ResMrpWorkOrder(models.Model): # 如果工单包含了外协工序,需要预留数量 if self.move_raw_ids.move_orig_ids.subcontract_workorder_id: location_id = self.move_raw_ids.location_id - quant = self.move_raw_ids.lot_ids.quant_ids.filtered(lambda q: q.location_id.id == location_id.id) + quant = self.move_raw_ids.lot_ids.quant_ids.filtered( + lambda q: q.location_id.id == location_id.id) if quant.reserved_quantity == 0: self.env['stock.quant']._update_reserved_quantity( self.move_raw_ids.product_id, @@ -1706,7 +1712,8 @@ class ResMrpWorkOrder(models.Model): store=True, string='工序作业') individuation_page_ids = fields.Many2many('sf.work.individuation.page', string='个性化记录', related='routing_work_center_id.individuation_page_ids') - individuation_page_list = fields.Char('个性化记录', default='', compute='_compute_individuation_page_ids', store=True) + individuation_page_list = fields.Char('个性化记录', default='', compute='_compute_individuation_page_ids', + store=True) @api.depends('name') def _compute_routing_work_center_id(self): @@ -1727,6 +1734,7 @@ class ResMrpWorkOrder(models.Model): individuation_page_list = [item.code for item in mw.individuation_page_ids] if individuation_page_list: mw.individuation_page_list = list(set(individuation_page_list)) + # ============================================================================================= is_inspect = fields.Boolean('需送检', compute='_compute_is_inspect', store=True, default=False) @@ -1750,6 +1758,23 @@ class ResMrpWorkOrder(models.Model): self.check_ids.filtered(lambda ch: ch.is_inspect is True and ch.quality_state == 'waiting').write( {'quality_state': 'none'}) + @api.model + def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): + aggregate_field = 'create_date:max' + if aggregate_field not in fields: + fields.append(aggregate_field) + orderby = "create_date desc" + + return super(ResMrpWorkOrder, self).read_group( + domain, + fields, + groupby, + offset=offset, + limit=limit, + orderby=orderby, + lazy=lazy + ) + class CNCprocessing(models.Model): _name = 'sf.cnc.processing' @@ -2040,6 +2065,7 @@ class WorkPieceDelivery(models.Model): def _get_agv_route_type_selection(self): return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection'] + type = fields.Selection(selection=_get_agv_route_type_selection, string='类型') delivery_duration = fields.Float('配送时长', compute='_compute_delivery_duration') status = fields.Selection( diff --git a/sf_manufacturing/models/product_template.py b/sf_manufacturing/models/product_template.py index 3554967c..1af6226a 100644 --- a/sf_manufacturing/models/product_template.py +++ b/sf_manufacturing/models/product_template.py @@ -51,7 +51,7 @@ class ResProductMo(models.Model): # domain="[('materials_id', '=', materials_id)]") # cutting_tool_model_id.material_model_id server_product_process_parameters_id = fields.Many2one('sf.production.process.parameter', - string='表面工艺参数(服务产品)') + string='工艺参数(服务产品)') model_process_parameters_ids = fields.Many2many('sf.production.process.parameter', 'process_parameter_rel', string='表面工艺参数') diff --git a/sf_manufacturing/models/purchase_order.py b/sf_manufacturing/models/purchase_order.py index 044b68e1..a919b3be 100644 --- a/sf_manufacturing/models/purchase_order.py +++ b/sf_manufacturing/models/purchase_order.py @@ -66,6 +66,37 @@ class PurchaseOrder(models.Model): raise UserError('请对【产品】中的【数量】进行输入') if line.price_unit <= 0: raise UserError('请对【产品】中的【单价】进行输入') + if record.purchase_type == 'consignment': + bom_line_id = record.order_line[0].purchase_request_lines.request_id.bom_id.bom_line_ids + replenish = self.env['stock.warehouse.orderpoint'].search([ + ('product_id', '=', bom_line_id.product_id.id), + ( + 'location_id', '=', self.env.ref('sf_stock.stock_location_outsourcing_material_receiving_area').id), + # ('state', 'in', ['draft', 'confirmed']) + ], limit=1) + if not replenish: + replenish_model = self.env['stock.warehouse.orderpoint'] + replenish = replenish_model.create({ + 'product_id': bom_line_id.product_id.id, + 'location_id': self.env.ref( + 'sf_stock.stock_location_outsourcing_material_receiving_area').id, + 'route_id': self.env.ref('sf_stock.stock_route_process_outsourcing').id, + 'group_id': record.group_id.id, + 'qty_to_order': 1, + 'origin': record.name, + }) + else: + replenish.write({ + 'product_id': bom_line_id.product_id.id, + 'location_id': self.env.ref( + 'sf_stock.stock_location_outsourcing_material_receiving_area').id, + 'route_id': self.env.ref('sf_stock.stock_route_process_outsourcing').id, + 'group_id': record.group_id.id, + 'qty_to_order': 1 + replenish.qty_to_order, + 'origin': record.name + ',' + replenish.origin, + }) + replenish.action_replenish() + return super(PurchaseOrder, self).button_confirm() diff --git a/sf_manufacturing/models/purchase_request_line.py b/sf_manufacturing/models/purchase_request_line.py new file mode 100644 index 00000000..385594e4 --- /dev/null +++ b/sf_manufacturing/models/purchase_request_line.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +import base64 +import datetime +import logging +import json +import os +import re +import traceback +from operator import itemgetter + +import requests +from itertools import groupby +from collections import defaultdict, namedtuple + +from odoo import api, fields, models, SUPERUSER_ID, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare, float_round, float_is_zero, format_datetime + + +class PurchaseRequestLine(models.Model): + _inherit = 'purchase.request.line' + is_subcontract = fields.Boolean(string='是否外协') + + +class PurchaseRequest(models.Model): + _inherit = 'purchase.request' + bom_id = fields.Many2one('mrp.bom') diff --git a/sf_manufacturing/models/sf_production_common.py b/sf_manufacturing/models/sf_production_common.py index 113858c1..708199bb 100644 --- a/sf_manufacturing/models/sf_production_common.py +++ b/sf_manufacturing/models/sf_production_common.py @@ -1,12 +1,50 @@ # -*- coding: utf-8 -*- import logging from odoo import fields, models, api -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError +from odoo.tools import str2bool class SfProductionProcessParameter(models.Model): _inherit = 'sf.production.process.parameter' + outsourced_service_products = fields.One2many( + 'product.template', # 另一个模型的名称 + 'server_product_process_parameters_id', # 对应的 Many2one 字段名称 + string='外协服务产品' + ) + is_product_button = fields.Boolean(compute='_compute_is_product_button',default=False) + is_delete_button = fields.Boolean(compute='_compute_is_delete_button', default=False) + routing_id = fields.Many2one('mrp.routing.workcenter', string="工序") + @api.constrains('outsourced_service_products') + def _validate_partner_limit(self): + for record in self: + if len(record.outsourced_service_products) > 1: + raise ValidationError("工艺参数不能与多个产品关联") + @api.depends('outsourced_service_products') + def _compute_is_product_button(self): + for record in self: + if record.outsourced_service_products: + record.is_product_button = True + else: + record.is_product_button = False + + def has_wksp_prefix(self,code): + """ + 判断字符串是否以WKSP开头(不区分大小写) + :param text: 要检查的字符串 + :return: True/False + """ + return code.upper().startswith('WKSP') + @api.depends('outsourced_service_products','code') + def _compute_is_delete_button(self): + for record in self: + if record.outsourced_service_products and self.has_wksp_prefix(record.code): + record.is_delete_button = False + elif record.outsourced_service_products: + record.is_delete_button = True + else: + record.is_delete_button = True @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): if self._context.get('route_id'): @@ -21,3 +59,33 @@ class SfProductionProcessParameter(models.Model): domain = [('process_id', '=', routing.surface_technics_id.id), ('id', 'not in', parameter)] return self._search(domain, limit=limit, access_rights_uid=name_get_uid) return super()._name_search(name, args, operator, limit, name_get_uid) + + def action_create_service_product(self): + if self.id: # 如果是已存在的记录 + self.write({}) # 空写入会触发保存 + else: # 如果是新记录 + self = self.create(self._convert_to_write(self.read()[0])) + return { + 'type': 'ir.actions.act_window', + 'name': '向导名称', + 'res_model': 'product.creation.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_process_parameter_id': self.id}, # 传递当前记录ID + } + # + # return { + # 'name': '创建服务产品', + # 'type': 'ir.actions.act_window', + # 'res_model': 'product.product', + # 'view_mode': 'form', + # 'view_id': self.env.ref('product.product_normal_form_view').id, + # 'target': 'new', # 关键参数,使窗口以弹窗形式打开 + # 'context': { + # 'default_' + k: v for k, v in default_values.items() + # }, + # } + + def action_hide_service_products(self): + # self.outsourced_service_products.active = False + self.active = False diff --git a/sf_manufacturing/models/stock.py b/sf_manufacturing/models/stock.py index 2bf1ea35..3a1b3cd9 100644 --- a/sf_manufacturing/models/stock.py +++ b/sf_manufacturing/models/stock.py @@ -755,6 +755,24 @@ class StockPicking(models.Model): if move_id.product_id.tracking in ['serial', 'lot'] and not move_id.move_line_nosuggest_ids: move_id.action_show_details() + @api.model + def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): + aggregate_field = 'create_date:max' + if aggregate_field not in fields: + fields.append(aggregate_field) + + orderby = "create_date desc" + + return super(StockPicking, self).read_group( + domain, + fields, + groupby, + offset=offset, + limit=limit, + orderby=orderby, + lazy=lazy + ) + class ReStockMove(models.Model): _inherit = 'stock.move' diff --git a/sf_manufacturing/models/workorder_printer.py b/sf_manufacturing/models/workorder_printer.py new file mode 100644 index 00000000..46eb3b8b --- /dev/null +++ b/sf_manufacturing/models/workorder_printer.py @@ -0,0 +1,134 @@ +import qrcode +import base64 +import logging +import tempfile +import os +import platform +import socket +import subprocess +from io import BytesIO +from odoo import models, fields, api +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +class MrpWorkorder(models.Model): + _name = 'mrp.workorder' + _inherit = ['mrp.workorder', 'printing.utils'] + + def print_pdf(self, printer_config, pdf_data): + """跨平台打印函数,支持网络打印机(IP:端口)""" + # 将PDF数据保存到临时文件 + with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file: + pdf_binary = base64.b64decode(pdf_data) + temp_file.write(pdf_binary) + temp_file_path = temp_file.name + + _logger.info(f"开始打印PDF文件: {temp_file_path}") + + try: + # 获取打印机名称或IP地址 + printer_name = printer_config.printer_id.name + if not printer_name: + raise UserError('打印机名称未配置') + + # 使用打印机配置中的IP地址和端口 + printer_ip = printer_config.printer_id.ip_address + printer_port = printer_config.printer_id.port + _logger.info(f"使用网络打印机: IP={printer_ip}, 端口={printer_port}") + + if platform.system() == 'Windows': + _logger.info(f"Windows环境不支持网络打印机") + else: # Linux环境 + # try: + # import cups + + # # 处理网络打印机情况 + # _logger.info(f"Linux环境下连接网络打印机: {printer_ip}:{printer_port}") + + # # 创建连接 + # conn = cups.Connection() + + # # 检查打印机是否已经添加到系统 + # printers = conn.getPrinters() + # _logger.info(f"可用打印机列表: {list(printers.keys())}") + + # network_printer_name = f"IP_{printer_ip}_{printer_port}" + + # # 如果打印机不存在,尝试添加 + # if network_printer_name not in printers: + # _logger.info(f"添加网络打印机: {network_printer_name}") + # conn.addPrinter( + # network_printer_name, + # device=f"socket://{printer_ip}:{printer_port}", + # info=f"Network Printer {printer_ip}:{printer_port}", + # location="Network" + # ) + # # 设置打印机为启用状态 + # conn.enablePrinter(network_printer_name) + # _logger.info(f"网络打印机添加成功: {network_printer_name}") + + # # 打印文件 + # _logger.info(f"开始打印到网络打印机: {network_printer_name}") + # job_id = conn.printFile(network_printer_name, temp_file_path, "工单打印", {}) + # _logger.info(f"打印作业ID: {job_id}") + + + # except ImportError as ie: + # _logger.error(f"导入CUPS库失败: {str(ie)}") + + # 尝试使用lp命令打印 + try: + _logger.info("尝试使用lp命令打印...") + + # 使用socket设置打印 + cmd = f"lp -h {printer_ip}:{printer_port} -d {printer_name} {temp_file_path}" + + _logger.info(f"执行lp打印命令: {cmd}") + result = subprocess.run(cmd, shell=True, check=True, capture_output=True) + _logger.info(f"lp打印结果: {result.stdout.decode()}") + except Exception as e: + _logger.error(f"lp命令打印失败: {str(e)}") + raise UserError(f'打印失败,请安装cups打印库: pip install pycups 或确保lp命令可用') + + return True + except Exception as e: + _logger.error(f"打印失败详细信息: {str(e)}") + raise UserError(f'打印失败: {str(e)}') + finally: + # 清理临时文件 + if os.path.exists(temp_file_path): + try: + os.unlink(temp_file_path) + _logger.info(f"临时文件已清理: {temp_file_path}") + except Exception as e: + _logger.error(f"清理临时文件失败: {str(e)}") + + def _compute_state(self): + super(MrpWorkorder, self)._compute_state() + for workorder in self: + work_ids = workorder.production_id.workorder_ids.filtered(lambda w: w.routing_type == '装夹预调' or w.routing_type == '人工线下加工') + for wo in work_ids: + if wo.state == 'ready' and not wo.production_id.product_id.is_print_program: + # 触发打印程序 + pdf_data = self.processing_drawing + try: + if pdf_data: + # 获取默认打印机配置 + printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name), ('printer_id.type', '=', 'normal')], limit=1) + if not printer_config: + raise UserError('请先配置打印机') + + # 执行打印 + if self.print_pdf(printer_config, pdf_data): + wo.production_id.product_id.is_print_program = True + _logger.info(f"工单 {wo.name} 的PDF已成功打印") + + except Exception as e: + _logger.error(f'打印配置错误: {str(e)}') + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + is_print_program = fields.Boolean(string='是否打印程序', default=False) + diff --git a/sf_manufacturing/security/ir.model.access.csv b/sf_manufacturing/security/ir.model.access.csv index 982506a5..852aa05f 100644 --- a/sf_manufacturing/security/ir.model.access.csv +++ b/sf_manufacturing/security/ir.model.access.csv @@ -193,4 +193,6 @@ access_sf_programming_record,sf_programming_record,model_sf_programming_record,b access_sf_work_individuation_page,sf_work_individuation_page,model_sf_work_individuation_page,sf_base.group_sf_mrp_user,1,1,1,0 access_sf_work_individuation_page_group_plan_dispatch,sf_work_individuation_page_group_plan_dispatch,model_sf_work_individuation_page,sf_base.group_plan_dispatch,1,1,0,0 access_sf_sale_order_cancel_wizard,sf_sale_order_cancel_wizard,model_sf_sale_order_cancel_wizard,sf_base.group_sf_order_user,1,1,1,0 -access_sf_sale_order_cancel_line,sf_sale_order_cancel_line,model_sf_sale_order_cancel_line,sf_base.group_sf_order_user,1,0,1,1 \ No newline at end of file +access_sf_sale_order_cancel_line,sf_sale_order_cancel_line,model_sf_sale_order_cancel_line,sf_base.group_sf_order_user,1,0,1,1 + +access_product_creation_wizard,product_creation_wizard,model_product_creation_wizard,base.group_user,1,1,1,0 \ No newline at end of file diff --git a/sf_manufacturing/views/mrp_production_addional_change.xml b/sf_manufacturing/views/mrp_production_addional_change.xml index 020c2691..9a9db4ac 100644 --- a/sf_manufacturing/views/mrp_production_addional_change.xml +++ b/sf_manufacturing/views/mrp_production_addional_change.xml @@ -455,7 +455,7 @@ - sequence + create_date desc delivery_warning == 'warning' delivery_warning == 'overdue' diff --git a/sf_manufacturing/views/mrp_routing_workcenter_view.xml b/sf_manufacturing/views/mrp_routing_workcenter_view.xml index 67bc0c97..d1eb0618 100644 --- a/sf_manufacturing/views/mrp_routing_workcenter_view.xml +++ b/sf_manufacturing/views/mrp_routing_workcenter_view.xml @@ -22,6 +22,26 @@ + + + + + + + + + + +