Merge branch 'develop' of https://e.coding.net/jikimo-hn/jikimo_sfs/jikimo_sf into feature/制造功能优化

This commit is contained in:
mgw
2025-04-18 14:39:11 +08:00
60 changed files with 1291 additions and 108 deletions

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models

View File

@@ -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',
}

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import main

View File

@@ -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': '传输失败'}

View File

@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@@ -52,10 +52,10 @@ class JikimoWorkorderException(models.Model):
def _get_message(self, message_queue_ids): def _get_message(self, message_queue_ids):
contents, _ = super(JikimoWorkorderException, self)._get_message(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 action_id = self.env.ref('mrp.mrp_production_action').id
for index, content in enumerate(contents): for index, content in enumerate(contents):
exception_id = self.env['jikimo.workorder.exception'].browse(message_queue_ids[index].res_id) 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) contents[index] = content.replace('{{url}}', url)
return contents, message_queue_ids return contents, message_queue_ids

View File

@@ -206,7 +206,7 @@ class QualityCheck(models.Model):
('NG', 'NG') ('NG', 'NG')
], string='出厂检验报告结果', default='OK') ], string='出厂检验报告结果', default='OK')
measure_operator = fields.Many2one('res.users', string='操机员') 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') @api.depends('measure_line_ids')
def _compute_quality_manager(self): def _compute_quality_manager(self):

View File

@@ -8,6 +8,7 @@ class Printer(models.Model):
name = fields.Char(string='名称', required=True) name = fields.Char(string='名称', required=True)
ip_address = fields.Char(string='IP 地址', required=True) ip_address = fields.Char(string='IP 地址', required=True)
port = fields.Integer(string='端口', default=9100) port = fields.Integer(string='端口', default=9100)
type = fields.Selection([('zpl', 'ZPL'), ('normal', '普通')], string='类型', default='zpl')
class TableStyle(models.Model): class TableStyle(models.Model):

View File

@@ -2,7 +2,16 @@
import time, datetime import time, datetime
import hashlib import hashlib
from odoo import models from odoo import models
from typing import Optional
import socket 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): class Common(models.Model):
_name = 'sf.sync.common' _name = 'sf.sync.common'
@@ -92,3 +101,120 @@ class PrintingUtils(models.AbstractModel):
# host = "192.168.50.110" # 可以作为参数传入,或者在此配置 # host = "192.168.50.110" # 可以作为参数传入,或者在此配置
# port = 9100 # 可以作为参数传入,或者在此配置 # port = 9100 # 可以作为参数传入,或者在此配置
self.send_to_printer(host, port, zpl_code) 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)

View File

@@ -9,6 +9,7 @@
<field name="name"/> <field name="name"/>
<field name="ip_address"/> <field name="ip_address"/>
<field name="port"/> <field name="port"/>
<field name="type"/>
<!-- 其他字段... --> <!-- 其他字段... -->
</tree> </tree>
</field> </field>
@@ -24,6 +25,7 @@
<field name="name"/> <field name="name"/>
<field name="ip_address"/> <field name="ip_address"/>
<field name="port"/> <field name="port"/>
<field name="type"/>
</group> </group>
</sheet> </sheet>
</form> </form>

View File

@@ -14,10 +14,12 @@
<field name="name">原材料</field> <field name="name">原材料</field>
<field name="type">原材料</field> <field name="type">原材料</field>
</record> </record>
<record id="product_category_surface_technics_sf" model="product.category"> <record id="product_category_surface_technics_sf" model="product.category">
<field name="name">表面工艺</field> <field name="name">表面工艺</field>
<field name="type">表面工艺</field> <field name="type">表面工艺</field>
<field name="parent_id" ref="sf_manufacturing.product_category_outsource_process"/>
<field name="property_cost_method">fifo</field>
<field name="property_valuation">manual_periodic</field>
</record> </record>
<record id="product_category_cutting_tool_sf" model="product.category"> <record id="product_category_cutting_tool_sf" model="product.category">
@@ -40,10 +42,10 @@
<!-- <field name="company_id" ref="base.main_company"/>--> <!-- <field name="company_id" ref="base.main_company"/>-->
</record> </record>
<!-- <record id="res_users_bfm" model="res.users">--> <!-- <record id="res_users_bfm" model="res.users">-->
<!-- <field name="name">业务平台</field>--> <!-- <field name="name">业务平台</field>-->
<!--&lt;!&ndash; <field name="partner_id" ref="res_partner_bfm"/>&ndash;&gt;--> <!--&lt;!&ndash; <field name="partner_id" ref="res_partner_bfm"/>&ndash;&gt;-->
<!-- </record>--> <!-- </record>-->
<record id="product_functional_tool_sf" model="product.product"> <record id="product_functional_tool_sf" model="product.product">
<field name="name">功能刀具</field> <field name="name">功能刀具</field>

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -9,8 +9,9 @@
""", """,
'category': 'sf', 'category': 'sf',
'website': 'https://www.sf.jikimo.com', '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': [
'data/sequence.xml',
'data/stock_data.xml', 'data/stock_data.xml',
'views/product_template_management_view.xml', 'views/product_template_management_view.xml',
], ],

View File

@@ -0,0 +1,10 @@
<odoo>
<data>
<record id="sequence_production_process_parameter" model="ir.sequence">
<field name="name">工艺可选参数编码序列</field>
<field name="code">sf.production.process.parameter</field>
<field name="prefix">WKSP</field>
<field name="padding">9</field>
</record>
</data>
</odoo>

View File

@@ -1,2 +1,4 @@
# from . import product_template # from . import product_template
# from . import product_supplierinfo # from . import product_supplierinfo
from . import sf_production_common
from . import mrp_routing_workcenter

View File

@@ -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)

View File

@@ -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)

View File

@@ -39,7 +39,7 @@
attrs="{'invisible': [('categ_type', 'not in', ['成品','坯料', '原材料'])],'readonly': [('id', '!=', False)]}"/> attrs="{'invisible': [('categ_type', 'not in', ['成品','坯料', '原材料'])],'readonly': [('id', '!=', False)]}"/>
<field name="materials_type_id" string="型号" placeholder="请选择" options="{'no_create': True}" <field name="materials_type_id" string="型号" placeholder="请选择" options="{'no_create': True}"
attrs="{'invisible': [('categ_type', 'not in', ['成品','坯料', '原材料'])],'readonly': [('id', '!=', False)]}"/> attrs="{'invisible': [('categ_type', 'not in', ['成品','坯料', '原材料'])],'readonly': [('id', '!=', False)]}"/>
<field name="server_product_process_parameters_id" string="表面工艺参数" <field name="server_product_process_parameters_id" string="工艺参数"
options="{'no_create': True}" options="{'no_create': True}"
attrs="{'invisible': ['|',('categ_type', '!=', '表面工艺'),('categ_type', '=', False)]}"/> attrs="{'invisible': ['|',('categ_type', '!=', '表面工艺'),('categ_type', '=', False)]}"/>
<field name="cutting_tool_material_id" class="custom_required" <field name="cutting_tool_material_id" class="custom_required"

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
from ftplib import FTP import os
from ftplib import FTP, error_perm
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -52,8 +53,8 @@ class FtpController:
print(self.username, self.port, self.host, self.password) print(self.username, self.port, self.host, self.password)
ftp = FTP_P() ftp = FTP_P()
_logger.info("===================connect==================") _logger.info("===================connect==================")
# self.ftp.set_debuglevel(2) #打开调试级别2显示详细信息 # ftp.set_debuglevel(2) #打开调试级别2显示详细信息
ftp.set_pasv(0) # 0主动模式 1 #被动模式 # ftp.set_pasv(1) # 0主动模式 1 #被动模式
try: try:
ftp.connect(self.host, self.port) ftp.connect(self.host, self.port)
ftp.login(self.username, self.password) ftp.login(self.username, self.password)
@@ -128,3 +129,126 @@ class FtpController:
:return: :return:
""" """
self.ftp.delete(delpath) self.ftp.delete(delpath)
def transfer_nc_files(source_ftp_info, target_ftp_info, source_dir, target_dir, keep_dir=False):
"""
从源FTP服务器下载所有.nc文件并上传到目标FTP服务器,保持目录结构
Args:
source_ftp_info: dict, 源FTP连接信息 {host, port, username, password}
target_ftp_info: dict, 目标FTP连接信息 {host, port, username, password}
source_dir: str, 源FTP上的起始目录
target_dir: str, 目标FTP上的目标目录
keep_dir: bool, 是否保持目录结构,默认False
"""
try:
# 连接源FTP
source_ftp = FtpController(
source_ftp_info['host'],
source_ftp_info['port'],
source_ftp_info['username'],
source_ftp_info['password']
)
source_ftp.ftp.set_pasv(1)
# 连接目标FTP
target_ftp = FtpController(
target_ftp_info['host'],
target_ftp_info['port'],
target_ftp_info['username'],
target_ftp_info['password']
)
source_ftp.ftp.set_pasv(1)
# 递归遍历源目录
def traverse_dir(current_dir, relative_path=''):
source_ftp.ftp.cwd(current_dir)
file_list = source_ftp.ftp.nlst()
for item in file_list:
try:
# 尝试进入目录
source_ftp.ftp.cwd(f"{current_dir}/{item}")
# 如果成功则是目录
new_relative_path = os.path.join(relative_path, item)
# 在目标FTP创建对应目录
try:
if keep_dir:
target_ftp.ftp.mkd(f"{target_dir}/{new_relative_path}")
except:
pass # 目录可能已存在
# 递归遍历子目录
traverse_dir(f"{current_dir}/{item}", new_relative_path)
source_ftp.ftp.cwd('..')
except:
# 如果是.nc文件则传输
if item.lower().endswith('.nc'):
# 下载到临时文件
temp_path = f"/tmp/{item}"
with open(temp_path, 'wb') as f:
source_ftp.ftp.retrbinary(f'RETR {item}', f.write)
# 上传到目标FTP对应目录
if keep_dir:
target_path = f"{target_dir}/{relative_path}/{item}"
else:
target_path = f"{target_dir}/{item}"
with open(temp_path, 'rb') as f:
target_ftp.ftp.storbinary(f'STOR {target_path}', f)
# 删除临时文件
os.remove(temp_path)
logging.info(f"已传输文件: {item}")
# 清空目标目录下的所有内容
try:
target_ftp.ftp.cwd(target_dir)
files = target_ftp.ftp.nlst()
for f in files:
try:
# 尝试删除文件
target_ftp.ftp.delete(f)
except:
try:
# 如果删除失败,可能是目录,递归删除目录
def remove_dir(path):
target_ftp.ftp.cwd(path)
sub_files = target_ftp.ftp.nlst()
for sf in sub_files:
try:
target_ftp.ftp.delete(sf)
except:
remove_dir(f"{path}/{sf}")
target_ftp.ftp.cwd('..')
target_ftp.ftp.rmd(path)
remove_dir(f"{target_dir}/{f}")
except:
logging.error(f"无法删除 {f}")
pass
logging.info(f"已清空目标目录 {target_dir}")
except Exception as e:
logging.error(f"清空目标目录失败: {str(e)}")
return False
# 开始遍历
traverse_dir(source_dir)
logging.info("所有.nc文件传输完成")
return True
except Exception as e:
logging.error(f"传输过程出错: {str(e)}")
return False
finally:
# 关闭FTP连接
try:
source_ftp.ftp.quit()
target_ftp.ftp.quit()
except:
pass

View File

@@ -4,3 +4,4 @@ from . import sf_maintenance_oee
from . import sf_maintenance_logs from . import sf_maintenance_logs
from . import sf_equipment_maintenance_standards from . import sf_equipment_maintenance_standards
from . import sf_maintenance_requests from . import sf_maintenance_requests
from . import maintenance_printer

View File

@@ -0,0 +1,92 @@
import qrcode
import base64
from io import BytesIO
from odoo import models, fields, api
class MaintenanceEquipment(models.Model):
_name = 'maintenance.equipment'
_inherit = ['maintenance.equipment', 'printing.utils']
qr_code_image = fields.Binary(string='二维码', compute='_generate_qr_code')
@api.depends('name')
def _generate_qr_code(self):
for record in self:
# Generate QR code
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(record.name)
qr.make(fit=True)
qr_image = qr.make_image(fill_color="black", back_color="white")
# Encode the image data in base64
image_stream = BytesIO()
qr_image.save(image_stream, format="PNG")
encoded_image = base64.b64encode(image_stream.getvalue())
record.qr_code_image = encoded_image
def print_single_method(self):
print('self.name========== %s' % self.name)
self.ensure_one()
qr_code_data = self.qr_code_image
if not qr_code_data:
raise UserError("没有找到二维码数据。")
maintenance_equipment_name = self.name
# host = "192.168.50.110" # 可以根据实际情况修改
# port = 9100 # 可以根据实际情况修改
# 获取默认打印机配置
printer_config = self.env['printer.configuration'].sudo().search([('model', '=', self._name)], limit=1)
if not printer_config:
raise UserError('请先配置打印机')
host = printer_config.printer_id.ip_address
port = printer_config.printer_id.port
self.print_qr_code(maintenance_equipment_name, host, port)
def generate_zpl_code(self, code):
"""生成ZPL代码用于打印二维码标签
Args:
code: 需要编码的内容
Returns:
str: ZPL指令字符串
"""
zpl_code = "^XA\n" # 开始ZPL格式
# 设置打印参数
zpl_code += "^LH0,0\n" # 设置标签起始位置
zpl_code += "^CI28\n" # 设置中文编码
zpl_code += "^PW400\n" # 设置打印宽度为400点
zpl_code += "^LL300\n" # 设置标签长度为300点
# 打印标题
zpl_code += "^FO10,20\n" # 设置标题位置
zpl_code += "^A0N,30,30\n" # 设置字体大小
zpl_code += "^FD机床二维码^FS\n" # 打印标题文本
# 打印二维码
zpl_code += "^FO50,60\n" # 设置二维码位置
zpl_code += f"^BQN,2,8\n" # 设置二维码参数:模式2,放大倍数8
zpl_code += f"^FDLA,{code}^FS\n" # 二维码内容
# 打印编码文本
zpl_code += "^FO50,220\n" # 设置编码文本位置
zpl_code += "^A0N,25,25\n" # 设置字体大小
zpl_code += f"^FD编码: {code}^FS\n" # 打印编码文本
# 打印日期
zpl_code += "^FO50,260\n"
zpl_code += "^A0N,20,20\n"
zpl_code += f"^FD打印日期: {fields.Date.today()}^FS\n"
zpl_code += "^PQ1\n" # 打印1份
zpl_code += "^XZ\n" # 结束ZPL格式
return zpl_code

View File

@@ -826,6 +826,11 @@ class SfMaintenanceEquipment(models.Model):
image_lq_id = fields.Many2many('maintenance.equipment.image', 'equipment_lq_id', string='冷却方式', image_lq_id = fields.Many2many('maintenance.equipment.image', 'equipment_lq_id', string='冷却方式',
domain="[('type', '=', '冷却方式')]") domain="[('type', '=', '冷却方式')]")
ftp_host = fields.Char('FTP 主机')
ftp_port = fields.Char('FTP 端口')
ftp_username = fields.Char('FTP 用户名')
ftp_password = fields.Char('FTP 密码')
class SfRobotAxisNum(models.Model): class SfRobotAxisNum(models.Model):
_name = 'sf.robot.axis.num' _name = 'sf.robot.axis.num'

View File

@@ -1053,6 +1053,26 @@
</page> </page>
</xpath> </xpath>
<xpath expr="//group/field[@name='location']" position="after">
<field name="qr_code_image" widget="image" readonly="1" attrs="{'invisible': [('equipment_type', '!=', '机床')]}" />
<label for="print_single_method"/>
<div class="col-12 col-lg-6 o_setting_box" style="white-space: nowrap">
<button type="object" class="oe_highlight" name='print_single_method' string="打印机床二维码"
attrs="{'invisible': [('equipment_type', '!=', '机床')]}"/>
</div>
</xpath>
<xpath expr="//page[@name='maintenance']" position="after">
<page name="network_config" string="网络配置" attrs="{'invisible': [('equipment_type', '!=', '机床')]}" >
<group>
<group string="ftp配置">
<field name="ftp_host" string="主机"/>
<field name="ftp_port" string="端口"/>
<field name="ftp_username" string="用户名"/>
<field name="ftp_password" string="密码" password="True"/>
</group>
</group>
</page>
</xpath>
</data> </data>
</field> </field>
</record> </record>

View File

@@ -10,7 +10,8 @@
""", """,
'category': 'sf', 'category': 'sf',
'website': 'https://www.sf.jikimo.com', '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': [
'data/cron_data.xml', 'data/cron_data.xml',
'data/stock_data.xml', 'data/stock_data.xml',
@@ -18,6 +19,7 @@
'data/panel_data.xml', 'data/panel_data.xml',
'data/sf_work_individuation_page.xml', 'data/sf_work_individuation_page.xml',
'data/agv_scheduling_data.xml', 'data/agv_scheduling_data.xml',
'data/product_data.xml',
'security/group_security.xml', 'security/group_security.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'wizard/workpiece_delivery_views.xml', 'wizard/workpiece_delivery_views.xml',
@@ -28,6 +30,7 @@
'wizard/mrp_workorder_batch_replan_wizard_views.xml', 'wizard/mrp_workorder_batch_replan_wizard_views.xml',
'wizard/sf_programming_reason_views.xml', 'wizard/sf_programming_reason_views.xml',
'wizard/sale_order_cancel_views.xml', 'wizard/sale_order_cancel_views.xml',
'wizard/process_outsourcing.xml',
'views/mrp_views_menus.xml', 'views/mrp_views_menus.xml',
'views/agv_scheduling_views.xml', 'views/agv_scheduling_views.xml',
'views/stock_lot_views.xml', 'views/stock_lot_views.xml',
@@ -44,6 +47,7 @@
'views/sale_order_views.xml', 'views/sale_order_views.xml',
'views/mrp_workorder_batch_replan.xml', 'views/mrp_workorder_batch_replan.xml',
'views/purchase_order_view.xml', 'views/purchase_order_view.xml',
'views/product_template_views.xml',
], ],
'assets': { 'assets': {

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="1">
<record id="product_category_service" model="product.category">
<field name="name">服务</field>
<field name="parent_id" ref="product.product_category_all"/>
<field name="property_cost_method">fifo</field>
<field name="property_valuation">manual_periodic</field>
</record>
<record id="product_category_outsource_process" model="product.category">
<field name="name">工序外协</field>
<field name="parent_id" ref="sf_manufacturing.product_category_service"/>
<field name="property_cost_method">fifo</field>
<field name="property_valuation">manual_periodic</field>
</record>
<record id="product_category_outsource_other_process" model="product.category">
<field name="name">其他</field>
<field name="parent_id" ref="sf_manufacturing.product_category_outsource_process"/>
<field name="property_cost_method">fifo</field>
<field name="property_valuation">manual_periodic</field>
</record>
</data>
</odoo>

View File

@@ -17,3 +17,5 @@ from . import sale_order
from . import quick_easy_order from . import quick_easy_order
from . import purchase_order from . import purchase_order
from . import quality_check from . import quality_check
from . import purchase_request_line
from . import workorder_printer

View File

@@ -6,6 +6,7 @@ import json
import os import os
import re import re
import traceback import traceback
from operator import itemgetter
import requests import requests
from itertools import groupby from itertools import groupby
@@ -238,7 +239,8 @@ class MrpProduction(models.Model):
programming_no = fields.Char('编程单号') programming_no = fields.Char('编程单号')
work_state = fields.Char('业务状态') work_state = fields.Char('业务状态')
programming_state = fields.Selection( programming_state = fields.Selection(
[('编程中', '编程中'), ('已编程', '已编程'), ('已编程未下发', '已编程未下发'), ('已下发', '已下发'), ('已取消', '已取消')], [('编程中', '编程中'), ('已编程', '已编程'), ('已编程未下发', '已编程未下发'), ('已下发', '已下发'),
('已取消', '已取消')],
string='编程状态', string='编程状态',
tracking=True) tracking=True)
glb_file = fields.Binary("glb模型文件") glb_file = fields.Binary("glb模型文件")
@@ -267,6 +269,7 @@ class MrpProduction(models.Model):
quality_standard = fields.Binary('质检标准', related='product_id.quality_standard', readonly=True) quality_standard = fields.Binary('质检标准', related='product_id.quality_standard', readonly=True)
part_name = fields.Char(string='零件名称', compute='_compute_part_info', store=True) part_name = fields.Char(string='零件名称', compute='_compute_part_info', store=True)
@api.depends('product_id') @api.depends('product_id')
def _compute_part_info(self): def _compute_part_info(self):
try: try:
@@ -400,8 +403,10 @@ class MrpProduction(models.Model):
and production.schedule_state == '已排' and production.is_rework is False): and production.schedule_state == '已排' and production.is_rework is False):
production.state = 'pending_cam' production.state = 'pending_cam'
if any((wo.test_results == '返工' and wo.state == 'done' and 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))) (production.programming_state in ['已编程'] or (
or (wo.is_rework is True and wo.state == 'done' and production.programming_state in ['编程中', '已编程']) 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: for wo in production.workorder_ids) or production.is_rework is True:
production.state = 'rework' production.state = 'rework'
if any(wo.test_results == '报废' and wo.state == 'done' for wo in production.workorder_ids): if any(wo.test_results == '报废' and wo.state == 'done' for wo in production.workorder_ids):
@@ -889,11 +894,44 @@ class MrpProduction(models.Model):
workorders_values.append( workorders_values.append(
self.env[ self.env[
'mrp.workorder']._json_workorder_surface_process_str( '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 production.workorder_ids = workorders_values
for workorder in production.workorder_ids: for workorder in production.workorder_ids:
workorder.duration_expected = workorder._get_duration_expected() 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): def get_subcontract_pick_purchase(self):
production_all = self.sorted(lambda x: x.id) 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(): for product_id, pd in grouped_product_ids.items():
product_id_to_production_names[product_id] = [p.name for p in pd] product_id_to_production_names[product_id] = [p.name for p in pd]
sorted_workorders = None sorted_workorders = None
purchase_request_line = []
for production in production_all: for production in production_all:
proc_workorders = [] proc_workorders = []
process_parameter_workorder = self.env['mrp.workorder'].search( process_parameter_workorder = self.env['mrp.workorder'].search(
@@ -919,7 +958,10 @@ class MrpProduction(models.Model):
return return
for workorders in reversed(sorted_workorders): for workorders in reversed(sorted_workorders):
self.env['stock.picking'].create_outcontract_picking(workorders, production, 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): def _reset_work_order_sequence1(self, k):
@@ -1728,7 +1770,8 @@ class MrpProduction(models.Model):
raise UserError('仅支持选择单个制造订单进行编程申请,请重新选择') raise UserError('仅支持选择单个制造订单进行编程申请,请重新选择')
for production in self: for production in self:
if production.state not in ['confirmed', 'pending_cam'] or production.programming_state != '已编程': 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() cloud_programming = production._cron_get_programming_state()
if cloud_programming['programming_state'] in ['待编程', '已编程', '编程中']: if cloud_programming['programming_state'] in ['待编程', '已编程', '编程中']:
raise UserError("当前编程单正在重新编程,请注意查看当前制造订单的“编程记录”确认进度!") raise UserError("当前编程单正在重新编程,请注意查看当前制造订单的“编程记录”确认进度!")

View File

@@ -1,6 +1,7 @@
import logging import logging
from odoo import fields, models, api from odoo import fields, models, api
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools import str2bool
class ResMrpRoutingWorkcenter(models.Model): class ResMrpRoutingWorkcenter(models.Model):
@@ -24,10 +25,41 @@ class ResMrpRoutingWorkcenter(models.Model):
workcenter_ids = fields.Many2many('mrp.workcenter', 'rel_workcenter_route', required=True) workcenter_ids = fields.Many2many('mrp.workcenter', 'rel_workcenter_route', required=True)
bom_id = fields.Many2one('mrp.bom', required=False) bom_id = fields.Many2one('mrp.bom', required=False)
surface_technics_id = fields.Many2one('sf.production.process', string="表面工艺") 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) reserved_duration = fields.Float('预留时长', default=30, tracking=True)
is_outsource = fields.Boolean('外协', default=False) is_outsource = fields.Boolean('外协', default=False)
individuation_page_ids = fields.Many2many('sf.work.individuation.page', string='个性化记录') 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): def get_no(self):
international_standards = self.search( international_standards = self.search(
[('code', '!=', ''), ('active', 'in', [True, False])], [('code', '!=', ''), ('active', 'in', [True, False])],

View File

@@ -21,7 +21,16 @@ class ResWorkcenter(models.Model):
related='equipment_id.production_line_id', store=True) related='equipment_id.production_line_id', store=True)
is_process_outsourcing = fields.Boolean('工艺外协') is_process_outsourcing = fields.Boolean('工艺外协')
users_ids = fields.Many2many("res.users", 'users_workcenter', tracking=True) 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): def write(self, vals):
if 'users_ids' in vals: if 'users_ids' in vals:
old_users = self.users_ids old_users = self.users_ids

View File

@@ -19,7 +19,6 @@ from odoo.addons.sf_mrs_connect.models.ftp_operate import FtpController
class ResMrpWorkOrder(models.Model): class ResMrpWorkOrder(models.Model):
_inherit = 'mrp.workorder' _inherit = 'mrp.workorder'
_order = 'sequence asc'
_description = '工单' _description = '工单'
product_tmpl_name = fields.Char('坯料产品名称', related='production_bom_id.bom_line_ids.product_id.name') product_tmpl_name = fields.Char('坯料产品名称', related='production_bom_id.bom_line_ids.product_id.name')
@@ -108,7 +107,7 @@ class ResMrpWorkOrder(models.Model):
if cur_workorder.is_subcontract or cur_workorder.routing_type == '解除装夹' or cur_workorder.routing_type == '切割' or any( 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.processing_panel == cur_workorder.processing_panel and
detection_result.routing_type == cur_workorder.routing_type and detection_result.routing_type == cur_workorder.routing_type and
cur_workorder.tag_type !='重新加工' and cur_workorder.tag_type != '重新加工' and
detection_result.test_results != '合格' detection_result.test_results != '合格'
for detection_result in cur_workorder.production_id.detection_result_ids for detection_result in cur_workorder.production_id.detection_result_ids
): ):
@@ -124,7 +123,7 @@ class ResMrpWorkOrder(models.Model):
if cur_workorder.is_subcontract or cur_workorder.routing_type == '解除装夹' or cur_workorder.routing_type == '切割' or any( 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.processing_panel == cur_workorder.processing_panel and
detection_result.routing_type == cur_workorder.routing_type and detection_result.routing_type == cur_workorder.routing_type and
cur_workorder.tag_type !='重新加工' and cur_workorder.tag_type != '重新加工' and
detection_result.test_results != '合格' detection_result.test_results != '合格'
for detection_result in cur_workorder.production_id.detection_result_ids for detection_result in cur_workorder.production_id.detection_result_ids
): ):
@@ -429,6 +428,8 @@ class ResMrpWorkOrder(models.Model):
def _compute_surface_technics_purchase_ids(self): def _compute_surface_technics_purchase_ids(self):
for order in self: for order in self:
if order.routing_type == '表面工艺' and order.state not in ['cancel']: 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'), domain = [('purchase_type', '=', 'consignment'),
('origin', 'like', '%' + self.production_id.name + '%'), ('origin', 'like', '%' + self.production_id.name + '%'),
('state', '!=', 'cancel')] ('state', '!=', 'cancel')]
@@ -473,12 +474,13 @@ class ResMrpWorkOrder(models.Model):
def _get_surface_technics_purchase_ids(self): def _get_surface_technics_purchase_ids(self):
domain = [('origin', 'like', '%' + self.production_id.name + '%'), ('purchase_type', '=', 'consignment')] 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'] purchase_orders_id = self.env['purchase.order']
for po in purchase_orders: for po in purchase_orders:
for line in po.order_line: for line in po.order_line:
if line.product_id.server_product_process_parameters_id == self.surface_technics_parameters_id: 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 return purchase_orders_id
@@ -1200,6 +1202,7 @@ class ResMrpWorkOrder(models.Model):
'cmm_ids': production.workorder_ids.filtered(lambda t: t.routing_type == 'CNC加工').cmm_ids, 'cmm_ids': production.workorder_ids.filtered(lambda t: t.routing_type == 'CNC加工').cmm_ids,
}] }]
return workorders_values_str return workorders_values_str
def _process_compute_state(self): def _process_compute_state(self):
for workorder in self: for workorder in self:
# 如果工单的工序没有进行排序则跳出循环 # 如果工单的工序没有进行排序则跳出循环
@@ -1287,6 +1290,7 @@ class ResMrpWorkOrder(models.Model):
mo.get_move_line(workorder.production_id, workorder)) mo.get_move_line(workorder.production_id, workorder))
else: else:
workorder.state = 'waiting' workorder.state = 'waiting'
@api.depends('production_availability', 'blocked_by_workorder_ids', 'blocked_by_workorder_ids.state', @api.depends('production_availability', 'blocked_by_workorder_ids', 'blocked_by_workorder_ids.state',
'production_id.tool_state', 'production_id.schedule_state', 'sequence', 'production_id.tool_state', 'production_id.schedule_state', 'sequence',
'production_id.programming_state') 'production_id.programming_state')
@@ -1301,6 +1305,7 @@ class ResMrpWorkOrder(models.Model):
for check_id in workorder.check_ids: for check_id in workorder.check_ids:
if not check_id.is_inspect: if not check_id.is_inspect:
check_id.quality_state = 'none' check_id.quality_state = 'none'
# 重写工单开始按钮方法 # 重写工单开始按钮方法
def button_start(self): def button_start(self):
# 判断工单状态是否为等待组件 # 判断工单状态是否为等待组件
@@ -1552,7 +1557,8 @@ class ResMrpWorkOrder(models.Model):
# 如果工单包含了外协工序,需要预留数量 # 如果工单包含了外协工序,需要预留数量
if self.move_raw_ids.move_orig_ids.subcontract_workorder_id: if self.move_raw_ids.move_orig_ids.subcontract_workorder_id:
location_id = self.move_raw_ids.location_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: if quant.reserved_quantity == 0:
self.env['stock.quant']._update_reserved_quantity( self.env['stock.quant']._update_reserved_quantity(
self.move_raw_ids.product_id, self.move_raw_ids.product_id,
@@ -1706,7 +1712,8 @@ class ResMrpWorkOrder(models.Model):
store=True, string='工序作业') store=True, string='工序作业')
individuation_page_ids = fields.Many2many('sf.work.individuation.page', string='个性化记录', individuation_page_ids = fields.Many2many('sf.work.individuation.page', string='个性化记录',
related='routing_work_center_id.individuation_page_ids') 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') @api.depends('name')
def _compute_routing_work_center_id(self): 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] individuation_page_list = [item.code for item in mw.individuation_page_ids]
if individuation_page_list: if individuation_page_list:
mw.individuation_page_list = list(set(individuation_page_list)) mw.individuation_page_list = list(set(individuation_page_list))
# ============================================================================================= # =============================================================================================
is_inspect = fields.Boolean('需送检', compute='_compute_is_inspect', store=True, default=False) 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( self.check_ids.filtered(lambda ch: ch.is_inspect is True and ch.quality_state == 'waiting').write(
{'quality_state': 'none'}) {'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): class CNCprocessing(models.Model):
_name = 'sf.cnc.processing' _name = 'sf.cnc.processing'
@@ -2040,6 +2065,7 @@ class WorkPieceDelivery(models.Model):
def _get_agv_route_type_selection(self): def _get_agv_route_type_selection(self):
return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection'] return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection']
type = fields.Selection(selection=_get_agv_route_type_selection, string='类型') type = fields.Selection(selection=_get_agv_route_type_selection, string='类型')
delivery_duration = fields.Float('配送时长', compute='_compute_delivery_duration') delivery_duration = fields.Float('配送时长', compute='_compute_delivery_duration')
status = fields.Selection( status = fields.Selection(

View File

@@ -51,7 +51,7 @@ class ResProductMo(models.Model):
# domain="[('materials_id', '=', materials_id)]") # domain="[('materials_id', '=', materials_id)]")
# cutting_tool_model_id.material_model_id # cutting_tool_model_id.material_model_id
server_product_process_parameters_id = fields.Many2one('sf.production.process.parameter', 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', model_process_parameters_ids = fields.Many2many('sf.production.process.parameter', 'process_parameter_rel',
string='表面工艺参数') string='表面工艺参数')

View File

@@ -66,6 +66,37 @@ class PurchaseOrder(models.Model):
raise UserError('请对【产品】中的【数量】进行输入') raise UserError('请对【产品】中的【数量】进行输入')
if line.price_unit <= 0: if line.price_unit <= 0:
raise UserError('请对【产品】中的【单价】进行输入') 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() return super(PurchaseOrder, self).button_confirm()

View File

@@ -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')

View File

@@ -1,12 +1,50 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
from odoo import fields, models, api 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): class SfProductionProcessParameter(models.Model):
_inherit = 'sf.production.process.parameter' _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 @api.model
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
if self._context.get('route_id'): 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)] 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 self._search(domain, limit=limit, access_rights_uid=name_get_uid)
return super()._name_search(name, args, operator, limit, 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

View File

@@ -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: if move_id.product_id.tracking in ['serial', 'lot'] and not move_id.move_line_nosuggest_ids:
move_id.action_show_details() 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): class ReStockMove(models.Model):
_inherit = 'stock.move' _inherit = 'stock.move'

View File

@@ -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)

View File

@@ -194,3 +194,5 @@ access_sf_work_individuation_page,sf_work_individuation_page,model_sf_work_indiv
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_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_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 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
194
195
196
197
198

View File

@@ -455,7 +455,7 @@
<field name="inherit_id" ref="mrp.mrp_production_workorder_tree_editable_view"/> <field name="inherit_id" ref="mrp.mrp_production_workorder_tree_editable_view"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//tree" position="attributes"> <xpath expr="//tree" position="attributes">
<attribute name="default_order">sequence</attribute> <attribute name="default_order">create_date desc</attribute>
<attribute name="decoration-warning">delivery_warning == 'warning'</attribute> <attribute name="decoration-warning">delivery_warning == 'warning'</attribute>
<attribute name="decoration-danger">delivery_warning == 'overdue'</attribute> <attribute name="decoration-danger">delivery_warning == 'overdue'</attribute>
</xpath> </xpath>

View File

@@ -22,6 +22,26 @@
<field name="is_repeat"/> <field name="is_repeat"/>
<field name="reserved_duration"/> <field name="reserved_duration"/>
</field> </field>
<xpath expr="//notebook/page[1]" position="before">
<page string="可选工艺参数">
<field name="optional_process_parameters">
<tree editable="bottom">
<field name="is_product_button" invisible="1"/>
<field name="is_delete_button" invisible="1"/>
<field name="code" attrs="{'readonly': True}"/>
<field name="name"/>
<field name="outsourced_service_products" widget="many2many_tags"/>
<!-- 按钮列 -->
<button name="action_create_service_product" string="创建服务产品" type="object"
class="btn-primary"
attrs="{'invisible': [('is_product_button', '=', True)]}" context="{'default_process_parameter_id':id}"/>
<button name="action_hide_service_products" string="删除" type="object"
class="oe_highlight"
attrs="{'invisible': [('is_delete_button', '=', True)]}"/>
</tree>
</field>
</page>
</xpath>
</field> </field>
</record> </record>
</data> </data>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="view_product_template_form_inherit_sf_manufacturing">
<field name="name">product.template.product.form.inherit.sf_manufacture</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="sf_dlm_management.view_product_template_only_form_inherit_sf"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='general_information']/group/group[@name='group_standard_price']/field[@name='barcode']" position="after">
<field name="model_id" readonly="1"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@@ -50,6 +50,9 @@
<xpath expr="//field[@name='origin']" position="after"> <xpath expr="//field[@name='origin']" position="after">
<field name="retrospect_ref"/> <field name="retrospect_ref"/>
</xpath> </xpath>
<xpath expr="//tree" position="attributes">
<attribute name="default_order">create_date desc</attribute>
</xpath>
</field> </field>
</record> </record>

View File

@@ -6,3 +6,4 @@ from . import production_technology_re_adjust_wizard
from . import mrp_workorder_batch_replan_wizard from . import mrp_workorder_batch_replan_wizard
from . import sf_programming_reason from . import sf_programming_reason
from . import sale_order_cancel from . import sale_order_cancel
from . import process_outsourcing

View File

@@ -0,0 +1,42 @@
from odoo import models, fields, api
class ProductCreationWizard(models.TransientModel):
_name = 'product.creation.wizard'
_description = '产品创建向导'
# 唯一需要用户输入的字段:产品类别
categ_id = fields.Many2one(
'product.category',
string='产品类别',
required=True,default=lambda self: self.env.ref(
'sf_manufacturing.product_category_outsource_other_process',
raise_if_not_found=False
).sudo(),
)
process_parameter_id = fields.Many2one('sf.production.process.parameter')
def action_create_service_product(self):
# 获取产品分类(服务)
service_categ = self.env.ref('sf_manufacturing.product_category_outsource_other_process').sudo()
default_values = {
'detailed_type': 'service',
'name': f"{self.process_parameter_id.process_id.name}{self.process_parameter_id.name}",
'invoice_policy': 'delivery',
'categ_id': service_categ.id,
'description': f"基于{self.process_parameter_id.name}创建的服务产品",
'sale_ok': True, # 可销售
'purchase_ok': True, # 可采购
'server_product_process_parameters_id': self.process_parameter_id.id,
}
def action_create_product(self):
service_categ = self.env.ref('sf_manufacturing.product_category_outsource_other_process').sudo()
default_values = {
'detailed_type': 'service',
'name': f"{self.process_parameter_id.process_id.name}{self.process_parameter_id.name}",
'invoice_policy': 'delivery',
'categ_id': self.categ_id.id,
'description': f"基于{self.process_parameter_id.name}创建的服务产品",
'sale_ok': True, # 可销售
'purchase_ok': True, # 可采购
'server_product_process_parameters_id': self.process_parameter_id.id,
}
self.env['product.template'].create(default_values)

View File

@@ -0,0 +1,37 @@
<odoo>
<record id="view_product_creation_wizard_form" model="ir.ui.view">
<field name="name">product.creation.wizard.form</field>
<field name="model">product.creation.wizard</field>
<field name="arch" type="xml">
<form string="创建产品">
<sheet>
<group>
<field name="categ_id" required="1"/>
</group>
</sheet>
<footer>
<button
name="action_create_product"
string="创建"
type="object"
class="btn-primary"
/>
<button
string="取消"
special="cancel"
class="btn-secondary"
/>
</footer>
</form>
</field>
</record>
<!-- 向导动作 -->
<record id="action_product_creation_wizard" model="ir.actions.act_window">
<field name="name">快速创建产品</field>
<field name="res_model">product.creation.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="product.model_product_product"/>
</record>
</odoo>

View File

@@ -8,6 +8,7 @@ from odoo.http import request
from odoo.addons.sf_base.controllers.controllers import MultiInheritController from odoo.addons.sf_base.controllers.controllers import MultiInheritController
class Sf_Mrs_Connect(http.Controller, MultiInheritController): class Sf_Mrs_Connect(http.Controller, MultiInheritController):
@http.route('/api/cnc_processing/create', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, @http.route('/api/cnc_processing/create', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False,
@@ -22,6 +23,7 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController):
try: try:
res = {'status': 1, 'message': '成功'} res = {'status': 1, 'message': '成功'}
datas = request.httprequest.data datas = request.httprequest.data
model_id = None
ret = json.loads(datas) ret = json.loads(datas)
ret = json.loads(ret['result']) ret = json.loads(ret['result'])
logging.info('下发编程单:%s' % ret) logging.info('下发编程单:%s' % ret)
@@ -57,6 +59,7 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController):
res['message'] = '编程单号为%s的CNC程序文件从FTP拉取失败' % (ret['programming_no']) res['message'] = '编程单号为%s的CNC程序文件从FTP拉取失败' % (ret['programming_no'])
return json.JSONEncoder().encode(res) return json.JSONEncoder().encode(res)
for production in productions: for production in productions:
model_id = production.product_id.model_id # 一个编程单的制造订单对应同一个模型
production.write({'programming_state': '已编程', 'work_state': '已编程', 'is_rework': False}) production.write({'programming_state': '已编程', 'work_state': '已编程', 'is_rework': False})
for panel in ret['processing_panel'].split(','): for panel in ret['processing_panel'].split(','):
# 查询状态为进行中且工序类型为CNC加工的工单 # 查询状态为进行中且工序类型为CNC加工的工单
@@ -83,12 +86,16 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController):
# panel) # panel)
program_path_tmp_panel = os.path.join('/tmp', ret['folder_name'], 'return', panel) program_path_tmp_panel = os.path.join('/tmp', ret['folder_name'], 'return', panel)
files_panel = os.listdir(program_path_tmp_panel) files_panel = os.listdir(program_path_tmp_panel)
panel_file_path = ''
if files_panel: if files_panel:
for file in files_panel: for file in files_panel:
file_extension = os.path.splitext(file)[1] file_extension = os.path.splitext(file)[1]
if file_extension.lower() == '.pdf': if file_extension.lower() == '.pdf':
panel_file_path = os.path.join(program_path_tmp_panel, file) panel_file_path = os.path.join(program_path_tmp_panel, file)
logging.info('panel_file_path:%s' % panel_file_path) logging.info('panel_file_path:%s' % panel_file_path)
# 向编程单中添加二维码
request.env['printing.utils'].add_qr_code_to_pdf(panel_file_path, model_id, "扫码获取工单")
cnc_workorder.write({'cnc_worksheet': base64.b64encode(open(panel_file_path, 'rb').read())}) cnc_workorder.write({'cnc_worksheet': base64.b64encode(open(panel_file_path, 'rb').read())})
pre_workorder = productions.workorder_ids.filtered( pre_workorder = productions.workorder_ids.filtered(
lambda ap: ap.routing_type in ['装夹预调', '人工线下加工'] and ap.state not in ['done', 'rework' lambda ap: ap.routing_type in ['装夹预调', '人工线下加工'] and ap.state not in ['done', 'rework'
@@ -268,3 +275,6 @@ class Sf_Mrs_Connect(http.Controller, MultiInheritController):
request.cr.rollback() request.cr.rollback()
logging.info('get_cnc_processing_create error:%s' % e) logging.info('get_cnc_processing_create error:%s' % e)
return json.JSONEncoder().encode(res) return json.JSONEncoder().encode(res)

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding='UTF-8'?> <?xml version="1.0" encoding='UTF-8'?>
<odoo> <odoo>
<data noupdate="1">
<record model="ir.cron" id="ir_cron_sf_static_resource_datasync"> <record model="ir.cron" id="ir_cron_sf_static_resource_datasync">
<field name="name">制造-配置每日定时同步cloud的静态资源库</field> <field name="name">制造-配置每日定时同步cloud的静态资源库</field>
<field name="model_id" ref="model_res_config_settings"/> <field name="model_id" ref="model_res_config_settings"/>
@@ -220,4 +220,5 @@
<!-- <field name="numbercall">-1</field>--> <!-- <field name="numbercall">-1</field>-->
<!-- <field name="doall" eval="False"/>--> <!-- <field name="doall" eval="False"/>-->
<!-- </record>--> <!-- </record>-->
</data>
</odoo> </odoo>

View File

@@ -1135,8 +1135,10 @@ class sfProductionProcessParameter(models.Model):
[("code", '=', item['code']), ('active', 'in', [True, False])]) [("code", '=', item['code']), ('active', 'in', [True, False])])
process = self.env['sf.production.process'].search( process = self.env['sf.production.process'].search(
[('code', '=', item['process_id_code'])], limit=1) [('code', '=', item['process_id_code'])], limit=1)
production_process_parameter = self.search(
[("code", '=', item['code']), ('active', 'in', [True, False])])
if not production_process_parameter: if not production_process_parameter:
self.create({ production_process_parameter = self.create({
"name": item['name'], "name": item['name'],
"process_description": item['process_description'], "process_description": item['process_description'],
"processing_day": item['processing_day'], "processing_day": item['processing_day'],
@@ -1148,6 +1150,7 @@ class sfProductionProcessParameter(models.Model):
[('materials_no', 'in', item['materials_model_ids_codes'])]), [('materials_no', 'in', item['materials_model_ids_codes'])]),
'processing_mm': item['processing_mm'] 'processing_mm': item['processing_mm']
}) })
production_process_parameter.create_service_product()
else: else:
production_process_parameter.name = item['name'] production_process_parameter.name = item['name']
production_process_parameter.process_description = item['process_description'] production_process_parameter.process_description = item['process_description']
@@ -1158,6 +1161,9 @@ class sfProductionProcessParameter(models.Model):
[('materials_no', 'in', item['materials_model_ids_codes'])]) [('materials_no', 'in', item['materials_model_ids_codes'])])
production_process_parameter.active = item['active'] production_process_parameter.active = item['active']
production_process_parameter.processing_mm = item['processing_mm'] production_process_parameter.processing_mm = item['processing_mm']
if not production_process_parameter.outsourced_service_products:
production_process_parameter.create_service_product()
production_process_parameter.create_work_center()
else: else:
raise ValidationError("表面工艺可选参数认证未通过") raise ValidationError("表面工艺可选参数认证未通过")

View File

@@ -343,6 +343,11 @@ class RePurchaseOrder(models.Model):
if order_line.product_id.id in product_list: if order_line.product_id.id in product_list:
purchase.purchase_type = 'outsourcing' purchase.purchase_type = 'outsourcing'
break break
request_lines = self.order_line.mapped('purchase_request_lines')
# 检查是否存在 is_subcontract 为 True 的行
if any(line.is_subcontract for line in request_lines):
purchase.purchase_type = 'consignment'
delivery_warning = fields.Selection([('normal', '正常'), ('warning', '预警'), ('overdue', '已逾期')], delivery_warning = fields.Selection([('normal', '正常'), ('warning', '预警'), ('overdue', '已逾期')],
string='交期状态', default='normal', string='交期状态', default='normal',
@@ -376,6 +381,27 @@ class RePurchaseOrder(models.Model):
if not line.taxes_id: if not line.taxes_id:
raise UserError('请对【产品】中的【税】进行选择') raise UserError('请对【产品】中的【税】进行选择')
def get_purchase_request(self, consecutive_process_parameters, production):
result = []
for pp in consecutive_process_parameters:
server_template = self.env['product.template'].search(
[('server_product_process_parameters_id', '=', pp.surface_technics_parameters_id.id),
('detailed_type', '=', 'service')])
result.append({
"product_id": server_template.product_variant_id.id,
"name": production.procurement_group_id.name,
"date_required": fields.Datetime.now(),
"product_uom_id":server_template.uom_id.id,
"product_qty": 1,
"request_id": False,
"move_dest_ids": False,
"orderpoint_id": False,
'is_subcontract':True,
'group_id':production.procurement_group_id.id,
'production_name':pp.production_id.name,
})
return result
def get_purchase_order(self, consecutive_process_parameters, production, product_id_to_production_names): def get_purchase_order(self, consecutive_process_parameters, production, product_id_to_production_names):
for pp in consecutive_process_parameters: for pp in consecutive_process_parameters:
server_product_process = [] server_product_process = []
@@ -396,14 +422,14 @@ class RePurchaseOrder(models.Model):
'manual_part_name': pp.part_name, 'manual_part_name': pp.part_name,
})) }))
# 获取服务商品最后一个供应商的采购员 # 获取服务商品最后一个供应商的采购员
purchase_user_id = server_template.seller_ids[-1].partner_id.purchase_user_id purchase_user_id = server_template.seller_ids[-1].partner_id.purchase_user_id if server_template.seller_ids else False
purchase_order = self.env['purchase.order'].sudo().create({ purchase_order = self.env['purchase.order'].sudo().create({
'partner_id': server_template.seller_ids[0].partner_id.id, 'partner_id': server_template.seller_ids[0].partner_id.id if server_template.seller_ids else False,
'origin': production.name, 'origin': production.name,
'state': 'draft', 'state': 'draft',
'purchase_type': 'consignment', 'purchase_type': 'consignment',
'order_line': server_product_process, 'order_line': server_product_process,
'user_id': purchase_user_id.id 'user_id': purchase_user_id.id if purchase_user_id else False,
}) })
purchase_order.order_line._compute_part_number() purchase_order.order_line._compute_part_number()
pp.purchase_id = [(6, 0, [purchase_order.id])] pp.purchase_id = [(6, 0, [purchase_order.id])]

View File

@@ -298,6 +298,10 @@
<xpath expr="//field[@name='name']" position="attributes"> <xpath expr="//field[@name='name']" position="attributes">
<attribute name="class">purchase_order_list_name</attribute> <attribute name="class">purchase_order_list_name</attribute>
</xpath> </xpath>
<!-- 修改 tree 视图的排序规则 -->
<xpath expr="//tree" position="attributes">
<attribute name="default_order">date_approve desc</attribute>
</xpath>
</field> </field>
</record> </record>

View File

@@ -14,16 +14,14 @@
<!-- name="Orders"--> <!-- name="Orders"-->
<!-- sequence="10">--> <!-- sequence="10">-->
<!-- <menuitem id="menu_sale_quotations"-->
<!-- action="action_quotations_with_onboarding"-->
<!-- groups="sales_team.group_sale_salesman"-->
<!-- sequence="10"/>-->
<!-- <menuitem id="menu_sale_order"--> <menuitem id="sale.menu_sale_quotations"
<!-- name="Orders"--> parent="sale.sale_order_menu"
<!-- action="action_orders"--> sequence="20"/>
<!-- groups="sales_team.group_sale_salesman"-->
<!-- sequence="20"/>--> <menuitem id="sale.menu_sale_order"
parent="sale.sale_order_menu"
sequence="10"/>
<!-- <menuitem id="report_sales_team"--> <!-- <menuitem id="report_sales_team"-->

View File

@@ -24,6 +24,7 @@
# always loaded # always loaded
'data': [ 'data': [
# 'security/ir.model.access.csv', # 'security/ir.model.access.csv',
'data/stock_location_data.xml',
'views/stock_picking.xml', 'views/stock_picking.xml',
'views/stock_product_template.xml', 'views/stock_product_template.xml',
], ],

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="stock_location_outsourcing_material_receiving_area" model="stock.location">
<field name="name">外协收料区</field>
<field name="usage">internal</field>
</record>
<record id="stock_route_process_outsourcing" model='stock.route'>
<field name="name">工序外协</field>
<field name="company_id"></field>
</record>
</data>
</odoo>

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import stock_picking from . import stock_picking
from . import stock_warehouse_orderpoint

View File

@@ -0,0 +1,11 @@
import logging
from odoo import models, fields
_logger = logging.getLogger(__name__)
class StockWarehouseOrderpoint(models.Model):
_inherit = 'stock.warehouse.orderpoint'
origin = fields.Char('补货来源')

View File

@@ -1242,7 +1242,7 @@ class FunctionalToolDismantle(models.Model):
functional_tool_id = fields.Many2one('sf.functional.cutting.tool.entity', '功能刀具', required=True, tracking=True, functional_tool_id = fields.Many2one('sf.functional.cutting.tool.entity', '功能刀具', required=True, tracking=True,
domain=[('functional_tool_status', '!=', '已拆除'), domain=[('functional_tool_status', '!=', '已拆除'),
('current_location', '=', '刀具房')]) ('current_location', 'in', ['刀具房', '线边刀库'])])
@api.onchange('functional_tool_id') @api.onchange('functional_tool_id')
def _onchange_functional_tool_id(self): def _onchange_functional_tool_id(self):

View File

@@ -10,6 +10,7 @@ from odoo.exceptions import ValidationError
class FunctionalCuttingToolEntity(models.Model): class FunctionalCuttingToolEntity(models.Model):
_name = 'sf.functional.cutting.tool.entity' _name = 'sf.functional.cutting.tool.entity'
_inherit = ['mail.thread']
_description = '功能刀具列表' _description = '功能刀具列表'
_order = 'functional_tool_status' _order = 'functional_tool_status'
@@ -41,7 +42,7 @@ class FunctionalCuttingToolEntity(models.Model):
max_lifetime_value = fields.Integer(string='最大寿命值(min)', readonly=True) max_lifetime_value = fields.Integer(string='最大寿命值(min)', readonly=True)
alarm_value = fields.Integer(string='报警值(min)', readonly=True) alarm_value = fields.Integer(string='报警值(min)', readonly=True)
used_value = fields.Integer(string='已使用值(min)', readonly=True) used_value = fields.Integer(string='已使用值(min)', readonly=True)
functional_tool_status = fields.Selection([('正常', '正常'), ('报警', '报警'), ('已拆除', '已拆除')], functional_tool_status = fields.Selection([('正常', '正常'), ('报警', '报警'), ('已拆除', '已拆除')], tracking=True,
string='状态', store=True, default='正常') string='状态', store=True, default='正常')
current_location_id = fields.Many2one('stock.location', string='当前位置', compute='_compute_current_location_id', current_location_id = fields.Many2one('stock.location', string='当前位置', compute='_compute_current_location_id',
store=True) store=True)
@@ -62,6 +63,17 @@ class FunctionalCuttingToolEntity(models.Model):
for item in self: for item in self:
if item: if item:
if item.functional_tool_status == '报警': if item.functional_tool_status == '报警':
self.create_tool_dismantle()
def set_functional_tool_status(self):
# self.write({
# 'functional_tool_status': '报警'
# })
self.functional_tool_status = '报警'
self.create_tool_dismantle()
def create_tool_dismantle(self):
for item in self:
# 创建报警刀具拆解单 # 创建报警刀具拆解单
self.env['sf.functional.tool.dismantle'].sudo().create({ self.env['sf.functional.tool.dismantle'].sudo().create({
'functional_tool_id': item.ids[0], 'functional_tool_id': item.ids[0],
@@ -263,7 +275,7 @@ class FunctionalCuttingToolEntity(models.Model):
functional_tool_model_ids.append(functional_tool_model.id) functional_tool_model_ids.append(functional_tool_model.id)
return [(6, 0, functional_tool_model_ids)] return [(6, 0, functional_tool_model_ids)]
dismantle_num = fields.Integer('拆解单数量', compute='_compute_dismantle_num', store=True) dismantle_num = fields.Integer('拆解单数量', compute='_compute_dismantle_num', tracking=True, store=True)
dismantle_ids = fields.One2many('sf.functional.tool.dismantle', 'functional_tool_id', '拆解单') dismantle_ids = fields.One2many('sf.functional.tool.dismantle', 'functional_tool_id', '拆解单')
@api.depends('dismantle_ids') @api.depends('dismantle_ids')

View File

@@ -107,11 +107,17 @@ class SfMaintenanceEquipment(models.Model):
if functional_tool_id.current_location != '机内刀库': if functional_tool_id.current_location != '机内刀库':
# 对功能刀具进行移动到生产线 # 对功能刀具进行移动到生产线
functional_tool_id.tool_inventory_displacement_out() functional_tool_id.tool_inventory_displacement_out()
functional_tool_id.write({ data_tool = {
'max_lifetime_value': data['MaxLife'], 'max_lifetime_value': data['MaxLife'],
'used_value': data['UseLife'], 'used_value': data['UseLife'],
'functional_tool_status': tool_install_time.get(data['State']) 'functional_tool_status': tool_install_time.get(data['State'])
}) }
if (functional_tool_id.functional_tool_status != '报警'
and tool_install_time.get(data['State']) == '报警'):
functional_tool_id.write(data_tool)
functional_tool_id.create_tool_dismantle()
else:
functional_tool_id.write(data_tool)
else: else:
logging.info('获取的【%s】设备不存在!!!' % data['DeviceId']) logging.info('获取的【%s】设备不存在!!!' % data['DeviceId'])
else: else:

View File

@@ -22,15 +22,21 @@
color: #999; color: #999;
} }
} }
}
.custom_group:has(.text-success){
position: relative;
&::after{ &::after{
content: ''; content: '';
display: block; display: block;
width: 18px; width: 72px;
height: 18px; height: 72px;
background: url('/sf_tool_management/static/images/replaceIcon.png') no-repeat center center; background: url('/sf_tool_management/static/images/replaceIcon.png') no-repeat center center;
background-size: 100%; background-size: 100%;
position: absolute;
bottom: 20px;
left: 300px;
} }
} }
.o_field_widget.o_readonly_modifier.o_field_char.text-success[name=handle_freight_rfid] { .o_field_widget.o_readonly_modifier.o_field_char.text-success[name=handle_freight_rfid] {
display: flex; display: flex;

View File

@@ -42,6 +42,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form create="0" edit="0" delete="0"> <form create="0" edit="0" delete="0">
<header> <header>
<button name="set_functional_tool_status" string="报警" type="object" invisible="1"/>
<!-- <button name="enroll_functional_tool_entity" string="功能刀具注册" type="object"--> <!-- <button name="enroll_functional_tool_entity" string="功能刀具注册" type="object"-->
<!-- class="btn-primary"/>--> <!-- class="btn-primary"/>-->
<field name="functional_tool_status" widget="statusbar" statusbar_visible="正常,报警,已拆除"/> <field name="functional_tool_status" widget="statusbar" statusbar_visible="正常,报警,已拆除"/>
@@ -192,6 +193,10 @@
</page> </page>
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form> </form>
</field> </field>
</record> </record>

View File

@@ -432,7 +432,7 @@
<field name="name">功能刀具组装</field> <field name="name">功能刀具组装</field>
<field name="model">sf.functional.tool.assembly</field> <field name="model">sf.functional.tool.assembly</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree create="0" delete="0"> <tree create="0" delete="0" default_order="create_date desc">
<field name="assembly_order_code"/> <field name="assembly_order_code"/>
<field name="barcode_id" optional="hide"/> <field name="barcode_id" optional="hide"/>
<field name="code" optional="hide"/> <field name="code" optional="hide"/>
@@ -531,7 +531,7 @@
<div> <div>
<separator string="刀柄:" style="font-size: 13px;"/> <separator string="刀柄:" style="font-size: 13px;"/>
</div> </div>
<group> <group class="custom_group" >
<field name="handle_code_id" string="序列号" placeholder="请选择" <field name="handle_code_id" string="序列号" placeholder="请选择"
options="{'no_create': True, 'no_quick_create': True}"/> options="{'no_create': True, 'no_quick_create': True}"/>
<field name="handle_freight_rfid" string="Rfid" decoration-success="handle_freight_rfid"/> <field name="handle_freight_rfid" string="Rfid" decoration-success="handle_freight_rfid"/>
@@ -554,7 +554,7 @@
<separator string="整体式刀具:" style="font-size: 13px;"/> <separator string="整体式刀具:" style="font-size: 13px;"/>
</div> </div>
<group> <group>
<group> <group class="custom_group">
<field name="integral_freight_barcode_id" string="货位" decoration-success="integral_verify == True"/> <field name="integral_freight_barcode_id" string="货位" decoration-success="integral_verify == True"/>
<field name="integral_lot_id" string="批次"/> <field name="integral_lot_id" string="批次"/>
<field name="integral_product_id" string="名称"/> <field name="integral_product_id" string="名称"/>
@@ -582,8 +582,8 @@
<separator string="刀片:" style="font-size: 13px;"/> <separator string="刀片:" style="font-size: 13px;"/>
</div> </div>
<group> <group>
<group> <group class="custom_group">
<field name="blade_freight_barcode_id" string="货位"/> <field name="blade_freight_barcode_id" string="货位" decoration-success="blade_verify == True"/>
<field name="blade_lot_id" string="批次"/> <field name="blade_lot_id" string="批次"/>
<field name="blade_product_id" string="名称"/> <field name="blade_product_id" string="名称"/>
<field name="cutting_tool_blade_model_id" string="型号"/> <field name="cutting_tool_blade_model_id" string="型号"/>
@@ -607,8 +607,8 @@
<separator string="刀杆:" style="font-size: 13px;"/> <separator string="刀杆:" style="font-size: 13px;"/>
</div> </div>
<group> <group>
<group> <group class="custom_group">
<field name="bar_freight_barcode_id" string="货位"/> <field name="bar_freight_barcode_id" string="货位" decoration-success="bar_verify == True"/>
<field name="bar_lot_id" string="批次"/> <field name="bar_lot_id" string="批次"/>
<field name="bar_product_id" string="名称"/> <field name="bar_product_id" string="名称"/>
<field name="cutting_tool_cutterbar_model_id" string="型号"/> <field name="cutting_tool_cutterbar_model_id" string="型号"/>
@@ -631,8 +631,8 @@
<separator string="刀盘:" style="font-size: 13px;"/> <separator string="刀盘:" style="font-size: 13px;"/>
</div> </div>
<group> <group>
<group> <group class="custom_group">
<field name="pad_freight_barcode_id" string="货位"/> <field name="pad_freight_barcode_id" string="货位" decoration-success="pad_verify == True"/>
<field name="pad_lot_id" string="批次"/> <field name="pad_lot_id" string="批次"/>
<field name="pad_product_id" string="名称"/> <field name="pad_product_id" string="名称"/>
<field name="cutting_tool_cutterpad_model_id" string="型号"/> <field name="cutting_tool_cutterpad_model_id" string="型号"/>