diff --git a/jikimo_account_process/__manifest__.py b/jikimo_account_process/__manifest__.py index f701deb7..0c1b521c 100644 --- a/jikimo_account_process/__manifest__.py +++ b/jikimo_account_process/__manifest__.py @@ -3,8 +3,8 @@ 'name': "jikimo_account_process", 'summary': """ - Short (1 phrase/line) summary of the module's purpose, used as - subtitle on modules listing or apps.openerp.com""", + 处理会计凭证生成重复名称报错问题 + """, 'description': """ Long description of module's purpose diff --git a/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.css b/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.css index df6cdfb6..edaa2bb4 100644 --- a/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.css +++ b/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.css @@ -1,41 +1,99 @@ -.zoomed { - position: fixed !important; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) scale(10); + +.processing-capabilities-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 10px; + width: 100%; } -.many2many_flex { - display: flex; +.grid-item { + display: flex; + align-items: center; } -.many2many_flex>div { - margin-right: 15px; - display: flex; - flex-direction: column; - align-items: center; +.item-content { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} +/*控制图片大小*/ +.item-icon { + width: 50px; + height: 50px; + margin-bottom: 5px; } -.many2many_flex>div>:nth-child(2) { - position: relative; +.item-label { + font-size: 12px; + word-break: break-word; } -.close { - width: 20px; - height: 20px; - position: absolute; - top: -8.8px; - right: -8.8px; - color: #fff; - background-color: #000; - opacity: 0; - text-align: center; - line-height: 20px; - font-size: 18px; +@media (max-width: 1200px) { + .processing-capabilities-grid { + grid-template-columns: repeat(4, 1fr); + } } -.img_close { - opacity: 1; - transform: scale(0.1); - cursor: pointer; +@media (max-width: 768px) { + .processing-capabilities-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 480px) { + .processing-capabilities-grid { + grid-template-columns: repeat(2, 1fr); + } +} +.image-preview-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; +} + +.image-preview-container.show { + opacity: 1; +} + +.image-preview { + max-width: 90%; + max-height: 90%; + object-fit: contain; + box-shadow: 0 0 20px rgba(255, 255, 255, 0.2); + border-radius: 5px; + transform: scale(0.9); + transition: transform 0.3s ease; +} + +.image-preview-container.show .image-preview { + transform: scale(1); +} + +.image-preview-close { + position: absolute; + top: 20px; + right: 30px; + color: #fff; + font-size: 40px; + font-weight: bold; + transition: 0.3s; + cursor: pointer; + opacity: 0.7; +} + +.image-preview-close:hover, +.image-preview-close:focus { + opacity: 1; + text-decoration: none; + cursor: pointer; } \ No newline at end of file diff --git a/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.js b/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.js index a3053b72..dee78c5f 100644 --- a/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.js +++ b/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.js @@ -4,35 +4,57 @@ import {Many2ManyCheckboxesField} from "@web/views/fields/many2many_checkboxes/m import {registry} from "@web/core/registry"; export class MyCustomWidget extends Many2ManyCheckboxesField { - // 你可以重写或者添加一些方法和属性 - // 例如,你可以重写setup方法来添加一些事件监听器或者初始化一些变量 setup() { - super.setup(); // 调用父类的setup方法 - // 你自己的代码 + super.setup(); } - onImageClick(event) { - // 放大图片逻辑 - // 获取图片元素 - const img = event.target; - const close = img.nextSibling; + onImageClick(event, src) { + event.preventDefault(); + event.stopPropagation(); - // 实现放大图片逻辑 - // 比如使用 CSS 放大 - img.parentElement.classList.add('zoomed'); - close.classList.add('img_close'); - } + // 创建预览框 + const previewContainer = document.createElement('div'); + previewContainer.className = 'image-preview-container'; - onCloseClick(event) { - const close = event.target; - const img = close.previousSibling; - img.parentElement.classList.remove('zoomed'); - close.classList.remove('img_close'); + const previewImg = document.createElement('img'); + previewImg.src = src; + previewImg.className = 'image-preview'; + // 设置放大的预览图片大小 + previewImg.style.width = '600px'; + previewImg.style.height = 'auto'; // 保持宽高比 + + const closeButton = document.createElement('span'); + closeButton.innerHTML = '×'; + closeButton.className = 'image-preview-close'; + + previewContainer.appendChild(previewImg); + previewContainer.appendChild(closeButton); + document.body.appendChild(previewContainer); + + // 添加关闭预览的事件监听器 + const closePreview = () => { + previewContainer.classList.remove('show'); + setTimeout(() => { + document.body.removeChild(previewContainer); + }, 300); + }; + + closeButton.addEventListener('click', closePreview); + + // 点击预览框外部也可以关闭 + previewContainer.addEventListener('click', (e) => { + if (e.target === previewContainer) { + closePreview(); + } + }); + + // 使用 setTimeout 来触发过渡效果 + setTimeout(() => { + previewContainer.classList.add('show'); + }, 10); } } MyCustomWidget.template = "jikimo_frontend.MyCustomWidget"; -// MyCustomWidget.supportedTypes = ['many2many']; - -registry.category("fields").add("custom_many2many_checkboxes", MyCustomWidget); +registry.category("fields").add("custom_many2many_checkboxes", MyCustomWidget); \ No newline at end of file diff --git a/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.xml b/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.xml index bebae03b..9bb8797d 100644 --- a/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.xml +++ b/jikimo_frontend/static/src/fields/custom_many2many_checkboxes/custom_many2many_checkboxes.xml @@ -2,27 +2,22 @@ -
+
-
+
- - +
+ + +
-
- - -
×
-
-
-
- + \ No newline at end of file diff --git a/jikimo_frontend/static/src/scss/custom_style.scss b/jikimo_frontend/static/src/scss/custom_style.scss index d7e6414c..8eb76259 100644 --- a/jikimo_frontend/static/src/scss/custom_style.scss +++ b/jikimo_frontend/static/src/scss/custom_style.scss @@ -108,6 +108,10 @@ td.o_required_modifier { } .color_3 { + background-color: #808080; +} + +.color_4 { background-color: rgb(255, 150, 0); } diff --git a/jikimo_workorder_exception/__init__.py b/jikimo_workorder_exception/__init__.py new file mode 100644 index 00000000..c3d410ea --- /dev/null +++ b/jikimo_workorder_exception/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers diff --git a/jikimo_workorder_exception/__manifest__.py b/jikimo_workorder_exception/__manifest__.py new file mode 100644 index 00000000..4c63ed6d --- /dev/null +++ b/jikimo_workorder_exception/__manifest__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': '机企猫 工单异常记录', + 'version': '1.0', + 'summary': '记录工单的异常日志', + 'sequence': 1, + 'category': 'sf', + 'website': 'https://www.sf.jikimo.com', + 'depends': ['sf_manufacturing', 'sf_mrs_connect'], + 'data': [ + 'views/mrp_workorder_views.xml', + 'security/ir.model.access.csv', + ], + 'demo': [ + ], + 'license': 'LGPL-3', + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/jikimo_workorder_exception/controllers/__init__.py b/jikimo_workorder_exception/controllers/__init__.py new file mode 100644 index 00000000..deec4a8b --- /dev/null +++ b/jikimo_workorder_exception/controllers/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/jikimo_workorder_exception/controllers/main.py b/jikimo_workorder_exception/controllers/main.py new file mode 100644 index 00000000..03744ff9 --- /dev/null +++ b/jikimo_workorder_exception/controllers/main.py @@ -0,0 +1,89 @@ +from odoo import http, fields +from odoo.http import request +import json +import logging +from odoo.addons.sf_mrs_connect.controllers.controllers import Sf_Mrs_Connect +from odoo.addons.sf_manufacturing.controllers.controllers import Manufacturing_Connect + +_logger = logging.getLogger(__name__) + +class WorkorderExceptionConroller(http.Controller): + + @http.route('/AutoDeviceApi/BillError', type='json', auth='public', methods=['GET', 'POST'], csrf=False, + cors="*") + def workder_exception(self, **kw): + """ + 记录工单异常 + :param kw: + :return: + """ + _logger.info('workder_exception:%s' % kw) + try: + res = {'Succeed': True, 'ErrorCode': 0, 'Error': ''} + datas = request.httprequest.data + ret = json.loads(datas) + if not ret.get('RfidCode') or not ret.get('coding'): + res = {'Succeed': False, 'ErrorCode': 400, 'Error': '参数错误'} + return json.JSONEncoder().encode(res) + + # 通过RfidCode获取就绪的CNC工单 + workorder = request.env['mrp.workorder'].sudo().search([ + ('rfid_code', '=', ret['RfidCode']), + ('routing_type', '=', 'CNC加工'), + ]) + if not workorder: + res = {'Succeed': False, 'ErrorCode': 401, 'Error': '无效的工单'} + return json.JSONEncoder().encode(res) + + # 创建工单异常记录,关联工单 + request.env['jikimo.workorder.exception'].sudo().create({ + 'workorder_id': workorder.id, + 'exception_code': ret.get('coding'), + 'exception_content': ret.get('Error', '') + }) + + except Exception as e: + res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} + _logger.info('workder_exception error:%s' % e) + return json.JSONEncoder().encode(res) + + +class SfMrsConnectController(Sf_Mrs_Connect): + + @http.route('/api/cnc_processing/create', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, + cors="*") + def get_cnc_processing_create(self, **kw): + """ + 更新工单异常记录【'YC001', 'YC004'】 + """ + res = super(SfMrsConnectController, self).get_cnc_processing_create(**kw) + # 如果有未完成的YC0001、YC0004异常记录,则标记为完成 + res = json.loads(res) + _logger.info('已进入工单异常:%s' % res) + if res.get('production_ids'): + try: + productions = request.env['mrp.production'].sudo().search([('id', 'in', res.get('production_ids'))]) + if productions.workorder_ids: + productions.workorder_ids.handle_exception(['YC0001', 'YC0004']) + except Exception as e: + _logger.info('更新工单异常记录失败:%s' % e) + return json.JSONEncoder().encode(res) + +class ManufactruingController(Manufacturing_Connect): + + @http.route('/AutoDeviceApi/FeedBackStart', type='json', auth='none', methods=['GET', 'POST'], csrf=False, + cors="*") + def button_Work_START(self, **kw): + """ + 更新工单异常记录【'YC0002', 'YC0003'】 + """ + res = super(ManufactruingController, self).button_Work_START(**kw) + res = json.loads(res) + _logger.info('已进入工单异常:%s' % res) + if res.get('workorder_id'): + try: + workorder = request.env['mrp.workorder'].sudo().browse(int(res.get('workorder_id'))) + workorder.handle_exception(['YC0002', 'YC0003']) + except Exception as e: + _logger.info('更新工单异常记录失败:%s' % e) + return json.JSONEncoder().encode(res) \ No newline at end of file diff --git a/jikimo_workorder_exception/models/__init__.py b/jikimo_workorder_exception/models/__init__.py new file mode 100644 index 00000000..9ab4d803 --- /dev/null +++ b/jikimo_workorder_exception/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import jikimo_workorder_exception +from . import mrp_workorder diff --git a/jikimo_workorder_exception/models/jikimo_workorder_exception.py b/jikimo_workorder_exception/models/jikimo_workorder_exception.py new file mode 100644 index 00000000..19dd824b --- /dev/null +++ b/jikimo_workorder_exception/models/jikimo_workorder_exception.py @@ -0,0 +1,14 @@ +from odoo import models, fields + + +class JikimoWorkorderException(models.Model): + _name = 'jikimo.workorder.exception' + _description = '工单异常记录' + _order = 'id desc' + + workorder_id = fields.Many2one('mrp.workorder', string='工单') + exception_code = fields.Char('异常编码') + exception_content = fields.Char('反馈的异常/问题信息') + completion_time = fields.Datetime('处理完成时间') + state = fields.Selection([('pending', '进行中'), ('done', '已处理')], string='状态', default='pending') + \ No newline at end of file diff --git a/jikimo_workorder_exception/models/mrp_workorder.py b/jikimo_workorder_exception/models/mrp_workorder.py new file mode 100644 index 00000000..64f0dd8f --- /dev/null +++ b/jikimo_workorder_exception/models/mrp_workorder.py @@ -0,0 +1,40 @@ +from odoo import models, fields +import logging + +_logger = logging.getLogger(__name__) + +class MrpWorkorder(models.Model): + _inherit = 'mrp.workorder' + + exception_ids = fields.One2many('jikimo.workorder.exception', 'workorder_id', string='工单异常记录') + + def write(self, values): + if values.get('test_results') and self.exception_ids: + pending_exception = self.exception_ids.filtered( + lambda exc: exc.state == 'pending' and exc.exception_code == 'YC0005' + ) + if pending_exception: + pending_exception.write({ + 'completion_time': fields.Datetime.now(), + 'state': 'done' + }) + return super(MrpWorkorder, self).write(values) + + def handle_exception(self, exception_codes): + """ + 处理异常 + :param exception_codes: 需要处理的异常编码列表 + """ + if not isinstance(exception_codes, list): + exception_codes = [exception_codes] + if self.exception_ids: + _logger.info('workorder.exception_ids:%s' % self.exception_ids) + pending_exception = self.exception_ids.filtered( + lambda exc: exc.state == 'pending' and exc.exception_code in exception_codes + ) + _logger.info('pending_exception:%s' % pending_exception) + if pending_exception: + pending_exception.write({ + 'completion_time': fields.Datetime.now(), + 'state': 'done' + }) diff --git a/jikimo_workorder_exception/security/ir.model.access.csv b/jikimo_workorder_exception/security/ir.model.access.csv new file mode 100644 index 00000000..660adb9c --- /dev/null +++ b/jikimo_workorder_exception/security/ir.model.access.csv @@ -0,0 +1,5 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_jikimo_workorder_exception","access.jikimo.workorder.exception","model_jikimo_workorder_exception","mrp.group_mrp_user",1,1,1,0 +"access_jikimo_workorder_exception_group_quality","access.jikimo.workorder.exception.group_quality","model_jikimo_workorder_exception","sf_base.group_quality",1,1,1,0 +"access_jikimo_workorder_exception_group_quality_director","access.jikimo.workorder.exception.group_quality_director","model_jikimo_workorder_exception","sf_base.group_quality_director",1,1,1,0 + diff --git a/jikimo_workorder_exception/tests/__init__.py b/jikimo_workorder_exception/tests/__init__.py new file mode 100644 index 00000000..99feb117 --- /dev/null +++ b/jikimo_workorder_exception/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_jikimo_workorder_exception diff --git a/jikimo_workorder_exception/tests/common.py b/jikimo_workorder_exception/tests/common.py new file mode 100644 index 00000000..bbc729db --- /dev/null +++ b/jikimo_workorder_exception/tests/common.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import fields, Command +from odoo.tests.common import TransactionCase, HttpCase, tagged, Form + +import json +import time +import base64 +from lxml import etree + +@tagged('post_install', '-at_install') +class TestJikimoWorkorderExceptionCommon(TransactionCase): + + def setUp(self): + super(TestJikimoWorkorderExceptionCommon, self).setUp() + # 获取名字为“1#自动生产线”的制造中心 + workcenter = self.env['mrp.workcenter'].search([('name', '=', '1#自动生产线')], limit=1) + # 创建一个产品 + product_product = self.env['product.product'].create({ + 'name': '测试产品', + 'type': 'product', + }) + uom_unit = self.env.ref('uom.product_uom_unit') + # 创建一个bom + self.bom = self.env['mrp.bom'].create({ + 'product_id': product_product.id, + 'product_tmpl_id': product_product.product_tmpl_id.id, + 'product_uom_id': uom_unit.id, + 'product_qty': 1.0, + 'type': 'normal', + }) + # 创建一个制造订单 + self.production = self.env['mrp.production'].create({ + 'name': 'Test Production', + 'product_id': product_product.id, + 'bom_id': self.bom.id, + 'company_id': self.env.ref('base.main_company').id, + }) + # 创建一个测试工单 + self.workorder = self.env['mrp.workorder'].create({ + 'name': 'Test order', + 'workcenter_id': workcenter.id, + 'product_uom_id': self.bom.product_uom_id.id, + 'production_id': self.production.id, + 'duration_expected': 1.0, + 'rfid_code': 'test-123456', + 'routing_type': 'CNC加工' + }) \ No newline at end of file diff --git a/jikimo_workorder_exception/tests/test_jikimo_workorder_exception.py b/jikimo_workorder_exception/tests/test_jikimo_workorder_exception.py new file mode 100644 index 00000000..69220947 --- /dev/null +++ b/jikimo_workorder_exception/tests/test_jikimo_workorder_exception.py @@ -0,0 +1,53 @@ +import json +from datetime import datetime +from odoo.addons.jikimo_workorder_exception.tests.common import TestJikimoWorkorderExceptionCommon + +class TestJikimoWorkorderException(TestJikimoWorkorderExceptionCommon): + + def test_create_exception_record(self): + exception_record = self.env['jikimo.workorder.exception'].create({ + 'workorder_id': self.workorder.id, + 'exception_code': 'YC0001', + 'exception_content': '无CNC编程' + }) + + self.assertTrue(exception_record) + self.assertEqual(exception_record.exception_content, '无CNC编程') + self.assertEqual(exception_record.workorder_id.id, self.workorder.id) + self.assertEqual(exception_record.exception_code, 'YC0001') + + def test_handle_exception(self): + exception_record = self.env['jikimo.workorder.exception'].create({ + 'workorder_id': self.workorder.id, + 'exception_code': 'YC0001', + 'exception_content': '无CNC编程' + }) + self.workorder.handle_exception('YC0001') + self.assertEqual(exception_record.state, 'done') + # 判断完成时间是否为当前分钟 + self.assertEqual(exception_record.completion_time.minute, datetime.now().minute) + + def test_handle_exception_with_invalid_code(self): + exception_record = self.env['jikimo.workorder.exception'].create({ + 'workorder_id': self.workorder.id, + 'exception_code': 'YC0001', + 'exception_content': '无CNC编程' + }) + self.workorder.handle_exception(['YC0002', 'YC0004']) + self.assertEqual(exception_record.state, 'pending') + self.assertEqual(exception_record.completion_time, False) + + + def test_handle_exception_with_test_results(self): + exception_record = self.env['jikimo.workorder.exception'].create({ + 'workorder_id': self.workorder.id, + 'exception_code': 'YC0005', + 'exception_content': '工单加工失败' + }) + self.workorder.write({ + 'test_results': '返工', + 'reason': 'cutter', + 'detailed_reason': '刀坏了', + }) + self.assertEqual(exception_record.state, 'done') + self.assertEqual(exception_record.completion_time.minute, datetime.now().minute) \ No newline at end of file diff --git a/jikimo_workorder_exception/views/mrp_workorder_views.xml b/jikimo_workorder_exception/views/mrp_workorder_views.xml new file mode 100644 index 00000000..e589adb4 --- /dev/null +++ b/jikimo_workorder_exception/views/mrp_workorder_views.xml @@ -0,0 +1,23 @@ + + + + + mrp.workorder.form + mrp.workorder + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jikimo_workorder_exception_notify/__init__.py b/jikimo_workorder_exception_notify/__init__.py new file mode 100644 index 00000000..77bbdbd3 --- /dev/null +++ b/jikimo_workorder_exception_notify/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models + diff --git a/jikimo_workorder_exception_notify/__manifest__.py b/jikimo_workorder_exception_notify/__manifest__.py new file mode 100644 index 00000000..0b8c1013 --- /dev/null +++ b/jikimo_workorder_exception_notify/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': '机企猫 工单异常消息通知', + 'version': '1.0', + 'summary': '当产生工单异常时,发送消息通知', + 'sequence': 1, + 'category': 'sf', + 'website': 'https://www.sf.jikimo.com', + 'depends': ['jikimo_workorder_exception', 'jikimo_message_notify'], + 'data': [ + 'data/bussiness_node.xml', + 'data/template_data.xml', + # 'security/ir.model.access.csv', + ], + 'demo': [ + ], + 'license': 'LGPL-3', + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/jikimo_workorder_exception_notify/data/bussiness_node.xml b/jikimo_workorder_exception_notify/data/bussiness_node.xml new file mode 100644 index 00000000..b772158b --- /dev/null +++ b/jikimo_workorder_exception_notify/data/bussiness_node.xml @@ -0,0 +1,17 @@ + + + + + 无功能刀具 + jikimo.workorder.exception + + + 无定位数据 + jikimo.workorder.exception + + + 加工失败 + jikimo.workorder.exception + + + \ No newline at end of file diff --git a/jikimo_workorder_exception_notify/data/template_data.xml b/jikimo_workorder_exception_notify/data/template_data.xml new file mode 100644 index 00000000..dec81e71 --- /dev/null +++ b/jikimo_workorder_exception_notify/data/template_data.xml @@ -0,0 +1,38 @@ + + + + + 生产线无功能刀具提醒 + + jikimo.workorder.exception + + markdown + urgent + ### 生产线无功能刀具提醒 +单号:工单[{{workorder_id.production_id.name}}]({{url}}) +原因:生产线无加工程序要用的功能刀具 + + + 工单无定位数据提醒 + + jikimo.workorder.exception + + markdown + urgent + ### 工单无定位数据提醒 +单号:工单[{{workorder_id.production_id.name}}]({{url}}) +原因:无装夹定位测量数据 + + + 工单加工失败提醒 + + jikimo.workorder.exception + + markdown + urgent + ### 工单加工失败提醒 +单号:工单[{{workorder_id.production_id.name}}]({{url}}) +原因:加工失败,工件下产线处理 + + + \ No newline at end of file diff --git a/jikimo_workorder_exception_notify/models/__init__.py b/jikimo_workorder_exception_notify/models/__init__.py new file mode 100644 index 00000000..76a90c74 --- /dev/null +++ b/jikimo_workorder_exception_notify/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import jikimo_message_template +from . import jikimo_workorder_exception diff --git a/jikimo_workorder_exception_notify/models/jikimo_message_template.py b/jikimo_workorder_exception_notify/models/jikimo_message_template.py new file mode 100644 index 00000000..530cbb99 --- /dev/null +++ b/jikimo_workorder_exception_notify/models/jikimo_message_template.py @@ -0,0 +1,10 @@ +from odoo import models + + +class JikimoMessageTemplate(models.Model): + _inherit = "jikimo.message.template" + + def _get_message_model(self): + res = super(JikimoMessageTemplate, self)._get_message_model() + res.append('jikimo.workorder.exception') + return res \ No newline at end of file diff --git a/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py b/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py new file mode 100644 index 00000000..9849f2cd --- /dev/null +++ b/jikimo_workorder_exception_notify/models/jikimo_workorder_exception.py @@ -0,0 +1,61 @@ +from odoo import models, api +from odoo.addons.sf_base.commons.common import Common +import requests, logging + +_logger = logging.getLogger(__name__) + + +class JikimoWorkorderException(models.Model): + _name = 'jikimo.workorder.exception' + _inherit = ['jikimo.workorder.exception', 'jikimo.message.dispatch'] + + @api.model_create_multi + def create(self, vals_list): + res = super(JikimoWorkorderException, self).create(vals_list) + # 根据异常编码发送消息提醒 + try: + for rec in res: + if rec.exception_code == 'YC0001': + # 无CNC程序,调用cloud接口 + data = {'name': rec.workorder_id.production_id.programming_no, 'exception_code': 'YC0001'} + configsettings = self.env['res.config.settings'].sudo().get_values() + config_header = Common.get_headers(self, configsettings['token'], configsettings['sf_secret_key']) + url = '/api/message/workorder_exception' + config_url = configsettings['sf_url'] + url + data['token'] = configsettings['token'] + ret = requests.post(config_url, json=data, headers=config_header) + ret = ret.json() + _logger.info('无CNC程序异常消息推送接口:%s' % ret) + elif rec.exception_code == 'YC0002': + # 无功能刀具 + rec.add_queue('无功能刀具') + elif rec.exception_code == 'YC0003': + # 无定位数据 + rec.add_queue('无定位数据') + elif rec.exception_code == 'YC0004': + # 无FTP文件,调用cloud接口 + data = {'name': rec.workorder_id.production_id.programming_no, 'exception_code': 'YC0004'} + configsettings = self.env['res.config.settings'].sudo().get_values() + config_header = Common.get_headers(self, configsettings['token'], configsettings['sf_secret_key']) + url = '/api/message/workorder_exception' + config_url = configsettings['sf_url'] + url + data['token'] = configsettings['token'] + ret = requests.post(config_url, json=data, headers=config_header) + ret = ret.json() + _logger.info('无FTP文件异常消息推送接口:%s' % ret) + elif rec.exception_code == 'YC0005': + # 加工失败 + rec.add_queue('加工失败') + except Exception as e: + _logger.error('异常编码发送消息提醒失败:%s' % e) + return res + + 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') + 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) + contents[index] = content.replace('{{url}}', url) + return contents diff --git a/jikimo_workorder_exception_notify/tests/__init__.py b/jikimo_workorder_exception_notify/tests/__init__.py new file mode 100644 index 00000000..318a4c8a --- /dev/null +++ b/jikimo_workorder_exception_notify/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_jikimo_workorder_exception_notify diff --git a/jikimo_workorder_exception_notify/tests/common.py b/jikimo_workorder_exception_notify/tests/common.py new file mode 100644 index 00000000..5fefe6bf --- /dev/null +++ b/jikimo_workorder_exception_notify/tests/common.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import fields, Command +from odoo.tests.common import TransactionCase, HttpCase, tagged, Form + +import json +import time +import base64 +from lxml import etree + +@tagged('post_install', '-at_install') +class TestJikimoWorkorderExceptionNotifyCommonNotify(TransactionCase): + + def setUp(self): + super(TestJikimoWorkorderExceptionNotifyCommonNotify, self).setUp() + # 获取最后一个工单 + self.workorder = self.env['mrp.workorder'].search([], order='id desc', limit=1) + \ No newline at end of file diff --git a/jikimo_workorder_exception_notify/tests/test_jikimo_workorder_exception_notify.py b/jikimo_workorder_exception_notify/tests/test_jikimo_workorder_exception_notify.py new file mode 100644 index 00000000..92886ff3 --- /dev/null +++ b/jikimo_workorder_exception_notify/tests/test_jikimo_workorder_exception_notify.py @@ -0,0 +1,113 @@ +import json +from datetime import datetime +from odoo.addons.jikimo_workorder_exception_notify.tests.common import TestJikimoWorkorderExceptionNotifyCommonNotify + +class TestJikimoWorkorderExceptionNotify(TestJikimoWorkorderExceptionNotifyCommonNotify): + + def test_create_message_template(self): + self.assertTrue(self.env['jikimo.message.template'].search([ + ('name', '=', '生产线无功能刀具提醒'), + ('model', '=', 'jikimo.workorder.exception') + ])) + self.assertTrue(self.env['jikimo.message.template'].search([ + ('name', '=', '工单无定位数据提醒'), + ('model', '=', 'jikimo.workorder.exception') + ])) + self.assertTrue(self.env['jikimo.message.template'].search([ + ('name', '=', '工单加工失败提醒'), + ('model', '=', 'jikimo.workorder.exception') + ])) + + def test_create_message_queue_yc0001(self): + exception_record = self.env['jikimo.workorder.exception'].create({ + 'workorder_id': self.workorder.id, + 'exception_code': 'YC0001', + 'exception_content': '无CNC程序' + }) + + message_record = self.env['jikimo.message.queue'].search([ + ('res_id', '=', exception_record.id), + ('model', '=', 'jikimo.workorder.exception'), + ('message_status', '=', 'pending') + ]) + self.assertFalse(message_record) + + def test_create_message_queue_yc0002(self): + exception_record = self.env['jikimo.workorder.exception'].create({ + 'workorder_id': self.workorder.id, + 'exception_code': 'YC0002', + 'exception_content': '无功能刀具' + }) + + bussiness_node = self.env['jikimo.message.bussiness.node'].search([ + ('name', '=', '无功能刀具'), + ('model', '=', 'jikimo.workorder.exception') + ]) + + message_template = self.env['jikimo.message.template'].search([ + ('bussiness_node_id', '=', bussiness_node.id), + ('model', '=', 'jikimo.workorder.exception') + ]) + + message_record = self.env['jikimo.message.queue'].search([ + ('res_id', '=', exception_record.id), + ('model', '=', 'jikimo.workorder.exception'), + ('message_status', '=', 'pending'), + ('message_template_id', '=', message_template.id) + ]) + self.assertTrue(message_record) + + def test_create_message_queue_yc0003(self): + exception_record = self.env['jikimo.workorder.exception'].create({ + 'workorder_id': self.workorder.id, + 'exception_code': 'YC0003', + 'exception_content': '无定位数据' + }) + + bussiness_node = self.env['jikimo.message.bussiness.node'].search([ + ('name', '=', '无定位数据'), + ('model', '=', 'jikimo.workorder.exception') + ]) + + message_template = self.env['jikimo.message.template'].search([ + ('bussiness_node_id', '=', bussiness_node.id), + ('model', '=', 'jikimo.workorder.exception') + ]) + + message_record = self.env['jikimo.message.queue'].search([ + ('res_id', '=', exception_record.id), + ('model', '=', 'jikimo.workorder.exception'), + ('message_status', '=', 'pending'), + ('message_template_id', '=', message_template.id) + ]) + self.assertTrue(message_record) + + def test_create_message_queue_yc0004(self): + exception_record = self.env['jikimo.workorder.exception'].create({ + 'workorder_id': self.workorder.id, + 'exception_code': 'YC0004', + 'exception_content': '无CNC程序' + }) + + message_record = self.env['jikimo.message.queue'].search([ + ('res_id', '=', exception_record.id), + ('model', '=', 'jikimo.workorder.exception'), + ('message_status', '=', 'pending') + ]) + self.assertFalse(message_record) + + def test_get_message(self): + exception_record = self.env['jikimo.workorder.exception'].create({ + 'workorder_id': self.workorder.id, + 'exception_code': 'YC0002', + 'exception_content': '无功能刀具' + }) + message_queue_ids = self.env['jikimo.message.queue'].search([ + ('res_id', '=', exception_record.id), + ('model', '=', 'jikimo.workorder.exception'), + ('message_status', '=', 'pending') + ]) + message = self.env['jikimo.workorder.exception']._get_message(message_queue_ids) + self.assertTrue(message) + + diff --git a/mrp_workorder/i18n/zh_CN.po b/mrp_workorder/i18n/zh_CN.po index 79a02876..18363c0a 100644 --- a/mrp_workorder/i18n/zh_CN.po +++ b/mrp_workorder/i18n/zh_CN.po @@ -1273,3 +1273,18 @@ msgstr "" #: model:product.template,description_sale:mrp_workorder.product_template_stool_top msgid "wooden stool top" msgstr "" + +#. module: mrp_workorder +#: model:quality.point.test_type,name:mrp_workorder.test_type_register_consumed_materials +msgid "Register Consumed Materials" +msgstr "登记消耗材料" + +#. module: mrp_workorder +#: model:quality.point.test_type,name:mrp_workorder.test_type_register_byproducts +msgid "Register By-products" +msgstr "按产品注册" + +#. module: mrp_workorder +#: model:quality.point.test_type,name:mrp_workorder.test_type_print_label +msgid "Print label" +msgstr "打印标签" \ No newline at end of file diff --git a/quality/i18n/zh_CN.po b/quality/i18n/zh_CN.po index 267c0196..68834fbc 100644 --- a/quality/i18n/zh_CN.po +++ b/quality/i18n/zh_CN.po @@ -1050,3 +1050,13 @@ msgstr "工作中心故障" #: model:ir.model.fields,field_description:quality.field_quality_point_test_type__active msgid "active" msgstr "有效" + +#. module: quality +#: model:quality.point.test_type,name:quality.test_type_instructions +msgid "Instructions" +msgstr "使用说明" + +#. module: quality +#: model:quality.point.test_type,name:quality.test_type_picture +msgid "Take a Picture" +msgstr "照片" \ No newline at end of file diff --git a/quality/models/quality.py b/quality/models/quality.py index 7edff2c7..83fd2258 100644 --- a/quality/models/quality.py +++ b/quality/models/quality.py @@ -15,7 +15,7 @@ class TestType(models.Model): _description = "Quality Control Test Type" # Used instead of selection field in order to hide a choice depending on the view. - name = fields.Char('Name', required=True) + name = fields.Char('Name', required=True,translate=True) technical_name = fields.Char('Technical name', required=True) active = fields.Boolean('active', default=True) diff --git a/quality_control/i18n/zh_CN.po b/quality_control/i18n/zh_CN.po index 8fd98237..d091d9b5 100644 --- a/quality_control/i18n/zh_CN.po +++ b/quality_control/i18n/zh_CN.po @@ -1185,3 +1185,14 @@ msgstr "请先进行质量检查!" #: model_terms:ir.ui.view,arch_db:quality_control.quality_alert_team_view_form msgid "e.g. The QA Masters" msgstr "例如:QA大师" + + +#. module: quality_control +#: model:quality.point.test_type,name:quality_control.test_type_passfail +msgid "Pass - Fail" +msgstr "通过-失败" + +#. module: quality_control +#: model:quality.point.test_type,name:quality_control.test_type_measure +msgid "Measure" +msgstr "测量" \ No newline at end of file diff --git a/quality_control/models/__init__.py b/quality_control/models/__init__.py index b31b19e7..c6817692 100644 --- a/quality_control/models/__init__.py +++ b/quality_control/models/__init__.py @@ -6,3 +6,4 @@ from . import stock_move from . import stock_move_line from . import stock_picking from . import stock_lot +from . import product_category \ No newline at end of file diff --git a/quality_control/models/product_category.py b/quality_control/models/product_category.py new file mode 100644 index 00000000..17f26006 --- /dev/null +++ b/quality_control/models/product_category.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from math import sqrt +from dateutil.relativedelta import relativedelta +from datetime import datetime + +import random + +from odoo import api, models, fields, _ +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_round +from odoo.osv.expression import OR + + +class ProductCategory(models.Model): + _inherit = 'product.category' + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + if args is None: + args = [] + # 添加过滤条件,确保只返回名称为 'abc' 的记录 + args += [('name', 'not in', ['Saleable', 'Expenses', 'Deliveries'])] + + # 调用父类的 name_search 方法 + return super(ProductCategory, self).name_search(name, args=args, operator=operator, limit=limit) + + @api.model + def search(self, args, limit=100, offset=0, order=None, count=False): + # 添加过滤条件,确保只返回名称不在指定列表中的记录 + args += [('name', 'not in', ['Saleable', 'Expenses', 'Deliveries'])] + + # 调用父类的 search 方法 + return super(ProductCategory, self).search(args, limit=limit, offset=offset, order=order, count=count) \ No newline at end of file diff --git a/quality_control/views/quality_views.xml b/quality_control/views/quality_views.xml index f7a1a3dc..fbd917b2 100644 --- a/quality_control/views/quality_views.xml +++ b/quality_control/views/quality_views.xml @@ -1033,7 +1033,7 @@ name="Overview" action="quality_alert_team_action" parent="menu_quality_root" - sequence="5"/> + sequence="5" active="False"/> 0 and cls not in cls._sub_classes: + cls.__bases__ = (cls._sub_classes[-1],) + if cls not in cls._sub_classes: + cls._sub_classes.append(cls) diff --git a/sf_base/models/base.py b/sf_base/models/base.py index c3152098..3d83cc96 100644 --- a/sf_base/models/base.py +++ b/sf_base/models/base.py @@ -394,3 +394,11 @@ class MachineToolCategory(models.Model): active = fields.Boolean('有效', default=True) category = fields.Selection([('shukong', u'数控'), ('putong', u'普通')], string=u'机床类别', default='shukong') + + +class MachiningAccuracy(models.Model): + _name = 'sf.machining.accuracy' + _description = '加工精度' + name = fields.Char('一般公差', index=True) + standard_tolerance = fields.Char(string="标准公差") + sync_id = fields.Char('同步ID') diff --git a/sf_base/security/ir.model.access.csv b/sf_base/security/ir.model.access.csv index 3d4ba3a0..34605cca 100644 --- a/sf_base/security/ir.model.access.csv +++ b/sf_base/security/ir.model.access.csv @@ -247,3 +247,7 @@ access_sf_cutting_tool_type_group_sf_stock_manager,sf_cutting_tool_type_group_sf access_sf_cutting_tool_material_group_plan_dispatch,sf_cutting_tool_material_group_plan_dispatch,model_sf_cutting_tool_material,sf_base.group_plan_dispatch,1,0,0,0 access_sf_functional_cutting_tool_model_group_plan_dispatch,sf_functional_cutting_tool_model_group_plan_dispatch,model_sf_functional_cutting_tool_model,sf_base.group_plan_dispatch,1,0,0,0 access_sf_cutting_tool_type_group_plan_dispatch,sf_cutting_tool_type_group_plan_dispatch,model_sf_cutting_tool_type,sf_base.group_plan_dispatch,1,0,0,0 + + +access_sf_machining_accuracy,sf_machining_accuracy,model_sf_machining_accuracy,base.group_user,1,0,0,0 +access_sf_machining_accuracy_admin,sf_machining_accuracy_admin,model_sf_machining_accuracy,base.group_system,1,0,0,0 diff --git a/sf_base/static/js/customTable.js b/sf_base/static/js/customTable.js new file mode 100644 index 00000000..a3da6f4f --- /dev/null +++ b/sf_base/static/js/customTable.js @@ -0,0 +1,125 @@ +// 获取表格数据 +function getDomData() { + const dom = $('div[name=cutting_speed_ids]') + if (!dom.length) return + const table = dom.find('.o_list_table') + const thead = table.children('thead') + const tbody = table.children('tbody') + const tbody_child = tbody.children() + const hideTheadDom = thead.find('[data-name=process_capability]') + hideTheadDom.hide().next().hide() + hideTheadDom.before('精加工粗加工') + tbody_child.each(function () { + const dom = $(this).children('[name=process_capability]') + if(!dom.length) return + dom.css('cssText', 'display: none!important').next().css('cssText', 'display: none!important') + const isCu = dom.text() == '粗加工' // 是否粗加工 + const v = dom.next().text() // 切削速度 + dom.after(`${!isCu ? v : ''}${isCu ? v : ''}`) + setListenClick() + }) +return; + handleTbody(tbody, newTableData, ΦList, table) +} + +// 监听点击 +function setListenClick() { + $(document).click(function (e) { + if ($(e.target).attr('customSpeed')) { + const orginV = $('[customInput=1]').children('input').val() + $('[customInput=1]').parent().html(orginV) + const v = $(e.target).attr('val') + const is = $(e.target).attr('is') + $(e.target).html('') + const input = $('
') + input.children('input').val(v) + $(e.target).append(input) + input.children('input').focus() + input.children('input').select() + } else if ($(e.target).attr('customInput')) { + + } else { + const orginV = $('[customInput=1]').children('input').val() + $('[customInput=1]').parent().html(orginV) + const v = $(e.target).attr('val') + } + }) + $(document).off('change') // 防止重复绑定 + $(document).on('change', '[customInput] input', async function () { + $(this).parents('td').attr('val', $(this).val()) + $(this).parents('td').siblings('[customspeed]').attr('val', $(this).val()) + var eve1 = new Event('change') + var eve2 = new Event('input') + var eve3 = new Event('click') + let patchSpeedDom = $(this).parents('td').siblings('[name=cutting_speed]') + let patchProcessDom = $(this).parents('td').siblings('[name=process_capability]') + $(this).parents('td').siblings('[customspeed]').text('') // 清空其他加工类型的数据 + await timeOut(500) + patchProcessDom[0].dispatchEvent(eve3) + await timeOut(200) + const processVal = $(this).parent().attr('is') + patchProcessDom.find('select').val(`"${processVal}"`) // 设置源select的val为“加工类型 is”、 + patchProcessDom.attr("data-tooltip", `${processVal}`) + patchProcessDom.find('select')[0].dispatchEvent(eve1) + + patchSpeedDom[0].dispatchEvent(eve3) + await timeOut(200) + patchSpeedDom.find('input').val($(this).val()) + await timeOut(50) + patchSpeedDom.find('input')[0].dispatchEvent(eve2) + patchSpeedDom.find('input')[0].dispatchEvent(eve1) + }) + $(document).off('blur') // 防止重复绑定 + $(document).on('blur', '[customInput] input', async function () { + if(!$(this).length) return + + $(this).parents('td').siblings('[customspeed]').text('') // 清空其他加工类型的数据 + let patchProcessDom = $(this).parents('td').siblings('[name=process_capability]') + try { + patchProcessDom[0].dispatchEvent(new Event('click')) + const processVal = $(this).parent().attr('is') + patchProcessDom.find('select').val(`"${processVal}"`) // 设置源select的val为“加工类型 is”、 + patchProcessDom.attr("data-tooltip", `${processVal}`) + patchProcessDom.find('select')[0].dispatchEvent(new Event('change')) + } catch { + + } + + }) +} +function timeOut(time) { + return new Promise(resolve => { + setTimeout(() => { + resolve() + }, time) + }) +} + +function listenAdd() { + $('td.o_field_x2many_list_row_add a').click(async function () { + await timeOut(500) + const tr = $('.o_list_table').children('tbody').children('tr').eq(-2) + if(tr.children('td').eq(2).text() == '') { + const dom = tr.children('[name=process_capability]') + if(!dom.length) return + dom.css('cssText', 'display: none!important').next().css('cssText', 'display: none!important') + const isCu = dom.text() == '粗加工' // 是否粗加工 + const v = dom.next().text() // 切削速度 + dom.after(`${!isCu ? v : ''}${isCu ? v : ''}`) + } + }) +} + +function listenSave() { + $('.o_form_button_save').click( async function () { + await timeOut(1000) + if($(this).parent().next().length) return + $('th[customTh],td[cusomSpeed]').remove() + getDomData() + + }) +} + +listenAdd() +listenSave() +getDomData() diff --git a/sf_base/static/js/setTableWidth.js b/sf_base/static/js/setTableWidth.js new file mode 100644 index 00000000..571d46cf --- /dev/null +++ b/sf_base/static/js/setTableWidth.js @@ -0,0 +1,47 @@ +// 因为表格可以拖动设置宽度,所以需要用js设置初始宽度 +function setBasicParamTableWidth() { + // const _100px = 'th[data-name="cutting_blade_length"],th[data-name="cutting_blade_length"],th[data-name="name"],th[data-name="tip_handling_size"],th[data-name="cutting_depth_max"],th[data-name="diameter_inner_circle"],th[data-name="diameter_mounting_hole" ],th[data-name="radius_tip_re" ],th[data-name="is_chip_breaker"],th[data-name="chip_breaker_type_code"],th[data-name="blade_profile"]' + // const _65px = 'th[data-name="edge_angle"],th[data-name="relief_angle"],[data-name="total_length"],th[data-name="length"],th[data-name="thickness"],th[data-name="blade_number"]' + // const _80px = 'th[data-name="arbor_diameter"],th[data-name="head_height"],th[data-name="head_width"],th[data-name="head_length"],th[data-name="blade_diameter"],th[data-name="blade_length"] ,th[data-name="neck_length"] ,th[data-name="neck_diameter"] ,th[data-name="shank_diameter"],th[data-name="shank_length"],th[data-name="tip_diameter"],th[data-name="knife_tip_taper"],th[data-name="blade_helix_angle"] ,th[data-name="blade_width"],th[data-name="blade_depth"]' + // const _50px = 'th[data-name="pitch"],th[data-name="width"],th[data-name="height"]' + + const basicParamDom = $('.fixTableCss') + // const basicParamDom_100px = basicParamDom.find(_100px) // 四字以上 + // const basicParamDom_65px = basicParamDom.find(_65px) // 大概三个字加单位 + // const basicParamDom_80px = basicParamDom.find(_80px) // 大概四个字加单位 + // const basicParamDom_50px= basicParamDom.find(_50px) // 大概两个字加单位 + // + // basicParamDom_100px.css({'width': '100px', 'max-width': 'auto', ',min-width': 'auto'}) + // basicParamDom_65px.css({'width': '65px', 'max-width': 'auto', ',min-width': 'auto'}) + // basicParamDom_80px.css({'width': '80px', 'max-width': 'auto', ',min-width': 'auto'}) + // basicParamDom_50px.css({'width': '50px', 'max-width': 'auto', ',min-width': 'auto'}) + let dom = [] + try { + dom = basicParamDom.find('table').find('thead').children().children() + + } catch { + dom = [] + } + if (!dom) return + dom.each(function () { + if ($(this).hasClass('row_no') >= 0) { // 序号列 + // 不设置 通过css设置 + } + const text = $(this).text().split('(') + if ($(this).attr('data-name') == 'name' || text[0].length > 4) { + $(this).width('100px') + } else if(text[0].length == 4){ + $(this).width('80px') + } else if(text[0].length == 3){ + $(this).width('65px') + } else if(text[0].length == 2){ + $(this).width('50px') + } + + }) +} + +setBasicParamTableWidth() +$('.o_field_many2one_selection').on('click', $('#cutting_tool_material_id + ul'), function () { + setTimeout(setBasicParamTableWidth, 500) +}) diff --git a/sf_base/static/js/updateTable.js b/sf_base/static/js/updateTable.js new file mode 100644 index 00000000..beec2c94 --- /dev/null +++ b/sf_base/static/js/updateTable.js @@ -0,0 +1,159 @@ +// 获取表格数据 +function getDomData() { + const dom = $('#updateTable').prev() + if (!dom.length) return + const table = $('#updateTable').prev().find('.o_list_table') + const customTable = table.clone() + customTable.addClass('customTable') + table.parent().append(customTable) + table.hide() + const thead = customTable.children('thead') + const tbody = customTable.children('tbody') + const tableData = [] + const tbody_child = tbody.children() + + const tbody_child_len = tbody_child.length + + for (let v = 0; v < tbody_child_len; v++) { // 将数据取出来到tableData里面 + const data = tbody_child[v].innerText.split('\t') + // console.log('dom data',data) + const [index, deep, name, Φ, value] = data + tableData.push({index, deep, name, Φ, value}) + } + const ΦList = [...new Set(tableData.map(_ => _.name))] // ΦList去重 + const newTableData = {} + tableData.forEach(_ => { + const key = _.deep + '|' + _.Φ + !newTableData[key] ? newTableData[key] = {i: _.index} : ''; + if (_.Φ) { // 去除没有Φ的脏数据 + newTableData[key]['Φ' + _.Φ] = _.value + newTableData[key]['Φ' + _.Φ + 'i'] = _.index + } + }) + // console.log('qwdh',tableData, ΦList, newTableData); + + if (ΦList.filter(_ => _).length == 0) return; + handleThead(thead, ΦList) + + handleTbody(tbody, newTableData, ΦList, table) +} + +// 重新设置表头、 +function handleThead(thead, ΦList) { + const dom = thead.children().eq(0).children() + const len = dom.length + dom.eq(0).attr('rowspan', 2) + dom.eq(1).attr('rowspan', 2) + len == 5 ? dom.eq(2).attr('rowspan', 2) : '' + dom.eq(-2).attr('colspan', ΦList.length) + dom.eq(-1).remove() + + const tr = document.createElement('tr') + for (let v = 0; v < ΦList.length; v++) { + const th = document.createElement('th') + th.innerText = 'Φ' + ΦList[v] + tr.append(th) + } + thead.append(tr) +} + +// 重新设置表格 +function handleTbody(tbody, newTableData, ΦList, table) { + console.log(newTableData) + tbody.html('') + let i = 0 + const data = Object.keys(newTableData) + // data.sort((a, b) => { + // a = a.split('=')[1].split('%')[0] + // b = b.split('=')[1].split('%')[0] + // return a - b + // }) + // console.log('wqoqw ',ΦList) + data.forEach(_ => { + i++ + const tr = $('') + const td0 = $('') + td0.text(i) + tr.append(td0) + const lit = _.split('|') + // + const td1 = $('') + const td2 = $('') + td1.text(lit[0]) + td2.text(lit[1] || '') + tr.append(td1) + tr.append(td2) + ΦList.forEach(Φ => { + const td = $('') + td.text(newTableData[_]['Φ' + Φ]) + td.attr('col', newTableData[_]['Φ' + Φ + 'i']) + td.attr('val', newTableData[_]['Φ' + Φ]) + td.attr('coustomTd', 1) + tr.append(td) + }) + // // for (let j = 0; j < ΦList.length; j++) { + // // const td = document.createElement('td') + // // td.innerText = newTableData[data[v]][_] + // // th.append(td) + // // } + tbody.append(tr) + }) + // $(document).click(function (e) { + // if ($(e.target).attr('coustomTd')) { + // const orginV = $('[coustomInput=1]').children('input').val() + // $('[coustomInput=1]').parent().html(orginV) + // const v = $(e.target).attr('val') + // console.log($(e.target)); + // $(e.target).html('') + // const input = $('
') + // input.children('input').val(v) + // $(e.target).append(input) + // input.children('input').focus() + // input.children('input').select() + // } else if ($(e.target).attr('coustomInput')) { + // + // } else { + // const orginV = $('[coustomInput=1]').children('input').val() + // $('[coustomInput=1]').parent().html(orginV) + // const v = $(e.target).attr('val') + // } + // }) + // $(document).off('change') // 防止重复绑定 +// $(document).on('change', '[coustomInput] input', function () { +// $(this).parents('td').attr('val', $(this).val()); +// var eve1 = new Event('change'); +// var eve2 = new Event('input'); +// var eve3 = new Event('click'); +// const i = $(this).parents('td').attr('col'); +// let patchDom = table.find('tbody').children('tr').eq(i - 1); +// +// if (patchDom.length === 0) { +// console.error('No such row found'); +// return; +// } +// +// patchDom = patchDom.children().eq(-1); +// +// setTimeout(() => { +// if (patchDom.length === 0) { +// console.error('No such cell found'); +// return; +// } +// patchDom[0].dispatchEvent(eve3); // Simulate click event +// +// setTimeout(() => { +// patchDom = patchDom.find('input'); +// if (patchDom.length === 0) { +// console.error('No input found in the target cell'); +// return; +// } +// patchDom.val($(this).val()); +// patchDom[0].dispatchEvent(eve2); +// patchDom[0].dispatchEvent(eve1); +// }, 200); +// }, 500); +// }); + +} + +getDomData() \ No newline at end of file diff --git a/sf_base/static/src/scss/change.scss b/sf_base/static/src/scss/change.scss new file mode 100644 index 00000000..6102b815 --- /dev/null +++ b/sf_base/static/src/scss/change.scss @@ -0,0 +1,52 @@ +.o_list_renderer .o_list_table tbody > tr > td:not(.o_list_record_selector):not(.o_handle_cell):not(.o_list_button):not(.o_list_record_remove) { + white-space: nowrap !important; +} + +.text-truncate { + overflow: unset !important; + text-overflow: unset !important; + white-space: unset !important; +} + + +// 设置表格不超出页面宽度 +.o_form_view .o_field_widget .o_list_renderer { + width: calc(100% - 64px) !important; + margin:0 auto; + overflow: auto; +} + +// 表格针对处理 +.fixTableCss { + text-align: center; + .o_list_number_th,.o_list_number { + text-align: center!important; + } + .ui-sortable { + tr > td:first-child { + padding: 0!important; + } + } + .row_no { + padding: 0!important;; + width: 35px!important; + } + + th[data-name="total_length"],th[data-name="length"],th[data-name="thickness"] { + .flex-row-reverse { + span { + text-align: center; + } + } + } +} + +// 其他不能用js处理的表格 +.otherTableFix { + th[data-name="cutting_tool_material_id"] { + width: 100px!important; + } + th[data-name="ramping_angle_max"],th[data-name="ramping_angle_min"] { + width: 200px!important; + } +} \ No newline at end of file diff --git a/sf_base/views/base_view.xml b/sf_base/views/base_view.xml index a41d3a2b..eb2c1571 100644 --- a/sf_base/views/base_view.xml +++ b/sf_base/views/base_view.xml @@ -109,7 +109,7 @@ form.sf.machine_tool.type sf.machine_tool.type -
+

@@ -129,31 +129,28 @@ - +
- + + + + - - - - - - + +
- + - - - + + + - - + +
@@ -178,7 +175,7 @@

- +
+ + + + + + + + + + - - - - - - 1 @@ -1142,14 +1169,16 @@
-
-
-
+
+
+
+

- + t-attf-class="#{record.run_status.raw_value == '运行中' ? 'font_color_1' : ''} + #{record.run_status.raw_value == '待机' ? 'font_color_4' : ''} + #{record.run_status.raw_value == '故障' ? 'font_color_2' : ''} + #{record.run_status.raw_value == '离线' ? 'font_color_3' : ''}"> +

@@ -1187,7 +1216,7 @@ - + diff --git a/sf_maintenance/wizard/__init__.py b/sf_maintenance/wizard/__init__.py new file mode 100644 index 00000000..d8d470dc --- /dev/null +++ b/sf_maintenance/wizard/__init__.py @@ -0,0 +1 @@ +from . import maintenance_request_wizard \ No newline at end of file diff --git a/sf_maintenance/wizard/maintenance_request_wizard.py b/sf_maintenance/wizard/maintenance_request_wizard.py new file mode 100644 index 00000000..e0f8d8fa --- /dev/null +++ b/sf_maintenance/wizard/maintenance_request_wizard.py @@ -0,0 +1,26 @@ +from odoo import fields, models + + +class MaintenanceRequestWizard(models.TransientModel): + _name = 'maintenance.request.wizard' + _description = '维保二次确认弹窗' + + name = fields.Char('') + + def submit(self): + context = self.env.context + equipment_id = self.env['maintenance.equipment'].sudo().search([('id', '=', context['equipment_id'])]) + request_ids = self.env['maintenance.request'].search([('stage_id.done', '=', False), + ('equipment_id', '=', equipment_id.id), + ('maintenance_type', '=', 'preventive'), + ('sf_maintenance_type', '=', context['type'])]) + request_ids.write({'active': False}) + return equipment_id.create_maintenance_request(context['type']) + + def cancel(self): + context = self.env.context + equipment_id = self.env['maintenance.equipment'].sudo().search([('id', '=', context['equipment_id'])]) + if context['type'] == '保养': + equipment_id.initial_action_date = equipment_id.initial_action_date_old + elif context['type'] == '检修': + equipment_id.initial_overhaul_date = equipment_id.initial_overhaul_date_old diff --git a/sf_maintenance/wizard/maintenance_request_wizard.xml b/sf_maintenance/wizard/maintenance_request_wizard.xml new file mode 100644 index 00000000..632c21d9 --- /dev/null +++ b/sf_maintenance/wizard/maintenance_request_wizard.xml @@ -0,0 +1,29 @@ + + + + 维保计划 + maintenance.request.wizard + form + new + + + + maintenance.request.wizard.form.view + maintenance.request.wizard + +
+
+ + 有未执行的历史维保计划,是否创建新维保计划!! +
+
+
+
+
+
+ + +
+
\ No newline at end of file diff --git a/sf_manufacturing/__manifest__.py b/sf_manufacturing/__manifest__.py index 35501367..fd15858a 100644 --- a/sf_manufacturing/__manifest__.py +++ b/sf_manufacturing/__manifest__.py @@ -10,7 +10,7 @@ """, 'category': 'sf', 'website': 'https://www.sf.jikimo.com', - 'depends': ['sf_base', 'sf_maintenance', 'web_widget_model_viewer', 'sf_warehouse'], + 'depends': ['sf_base', 'sf_maintenance', 'web_widget_model_viewer', 'sf_warehouse','jikimo_attachment_viewer'], 'data': [ 'data/stock_data.xml', 'data/empty_racks_data.xml', @@ -45,6 +45,8 @@ 'sf_manufacturing/static/src/scss/kanban_change.scss', 'sf_manufacturing/static/src/xml/button_show_on_tree.xml', 'sf_manufacturing/static/src/js/workpiece_delivery_wizard_confirm.js', + 'sf_manufacturing/static/src/js/qr.js', + 'sf_manufacturing/static/src/xml/qr.xml', ] }, diff --git a/sf_manufacturing/controllers/controllers.py b/sf_manufacturing/controllers/controllers.py index 48144622..2a5e4d3e 100644 --- a/sf_manufacturing/controllers/controllers.py +++ b/sf_manufacturing/controllers/controllers.py @@ -189,6 +189,7 @@ class Manufacturing_Connect(http.Controller): request.env['sf.production.plan'].sudo().search([('production_id', '=', production_id)]).write( {'actual_start_time': workorder.date_start, 'state': 'processing'}) + res.update({'workorder_id': workorder.id}) except Exception as e: res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} @@ -595,14 +596,6 @@ class Manufacturing_Connect(http.Controller): if panel_workorder: panel_workorder.write({'production_line_state': '已下产线'}) workorder.write({'state': 'to be detected'}) - # workpiece_delivery = request.env['sf.workpiece.delivery'].sudo().search( - # [ - # ('rfid_code', '=', rfid_code), ('type', '=', '下产线'), - # ('production_id', '=', order.production_id.id), - # ('workorder_id', '=', order.id), - # ('workorder_state', '=', 'done')]) - # if workpiece_delivery: - # delivery_Arr.append(workpiece_delivery.id) else: res = {'Succeed': False, 'ErrorCode': 204, 'Error': 'DeviceId为%s没有对应的已配送工件数据' % ret['DeviceId']} @@ -695,4 +688,4 @@ class Manufacturing_Connect(http.Controller): except Exception as e: res = {'Succeed': False, 'ErrorCode': 202, 'Error': str(e)} logging.info('AGVDownProduct error:%s' % e) - return json.JSONEncoder().encode(res) \ No newline at end of file + return json.JSONEncoder().encode(res) diff --git a/sf_manufacturing/models/mrp_production.py b/sf_manufacturing/models/mrp_production.py index 59ce68ab..571971bc 100644 --- a/sf_manufacturing/models/mrp_production.py +++ b/sf_manufacturing/models/mrp_production.py @@ -18,7 +18,7 @@ class MrpProduction(models.Model): _inherit = 'mrp.production' _description = "制造订单" _order = 'create_date desc' - + deadline_of_delivery = fields.Date('订单交期', tracking=True, compute='_compute_deadline_of_delivery') # tray_ids = fields.One2many('sf.tray', 'production_id', string="托盘") maintenance_count = fields.Integer(compute='_compute_maintenance_count', string="Number of maintenance requests") request_ids = fields.One2many('maintenance.request', 'production_id') @@ -34,6 +34,29 @@ class MrpProduction(models.Model): tool_state_remark = fields.Text(string='功能刀具状态备注(缺刀)', compute='_compute_tool_state_remark', store=True) tool_state_remark2 = fields.Text(string='功能刀具状态备注(无效刀)', readonly=True) + @api.depends('procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id') + def _compute_deadline_of_delivery(self): + for production in self: + # 确保 procurement_group_id 和相关字段存在 + if production.procurement_group_id: + # 获取相关的 sale_id + sale_order_id = production.procurement_group_id.mrp_production_ids.mapped( + 'move_dest_ids.group_id.sale_id') + + # 确保 sale_order_id 是有效的 ID 列表 + if sale_order_id: + # 获取 sale.order 记录 + sale_id = self.env['sale.order'].sudo().browse(sale_order_id.ids) # 使用 mapped 返回的 ID 列表 + + # 处理 sale_id + if sale_id: + # 假设我们只需要第一个 sale_id + production.deadline_of_delivery = sale_id[0].deadline_of_delivery if sale_id else False + else: + production.deadline_of_delivery = False + else: + production.deadline_of_delivery = False + @api.depends('workorder_ids.tool_state_remark') def _compute_tool_state_remark(self): for item in self: @@ -118,10 +141,12 @@ class MrpProduction(models.Model): ], string='工序状态', default='待装夹') # 零件图号 - part_number = fields.Char('零件图号') + part_number = fields.Char('零件图号', related='product_id.part_number', readonly=True) # 上传零件图纸 - part_drawing = fields.Binary('零件图纸') + part_drawing = fields.Binary('零件图纸', related='product_id.machining_drawings', readonly=True) + + quality_standard = fields.Binary('质检标准', related='product_id.quality_standard', readonly=True) @api.depends('product_id.manual_quotation') def _compute_manual_quotation(self): @@ -296,8 +321,13 @@ class MrpProduction(models.Model): # 编程单更新 def update_programming_state(self): try: + manufacturing_type = 'rework' + if self.is_scrap: + manufacturing_type = 'scrap' + elif self.tool_state == '2': + manufacturing_type = 'invalid_tool_rework' res = {'programming_no': self.programming_no, - 'manufacturing_type': 'rework' if self.is_scrap is False else 'scrap'} + 'manufacturing_type': manufacturing_type} logging.info('res=%s:' % res) configsettings = self.env['res.config.settings'].get_values() config_header = Common.get_headers(self, configsettings['token'], configsettings['sf_secret_key']) @@ -776,6 +806,8 @@ class MrpProduction(models.Model): 'date_to': date_planned_end, }) # work.write({'date_planned_start': date_planned_start, 'date_planned_finished': date_planned_end}) + # 设置一个较大的结束时间,防止在设置开始时间时,结束时间小于开始时间 + work.date_planned_finished = datetime.datetime.today() + datetime.timedelta(days=100) work.date_planned_start = date_planned_start work.date_planned_finished = date_planned_end routing_workcenter = self.env['mrp.routing.workcenter'].sudo().search( @@ -943,6 +975,8 @@ class MrpProduction(models.Model): if production.programming_no in program_to_production_names: productions_not_delivered = self.env['mrp.production'].search( [('programming_no', '=', production.programming_no), ('programming_state', '=', '已编程未下发')]) + productions = self.env['mrp.production'].search( + [('programming_no', '=', production.programming_no), ('state', 'not in', ('cancel', 'done'))]) rework_workorder = production.workorder_ids.filtered(lambda m: m.state == 'rework') if rework_workorder: for rework_item in rework_workorder: @@ -955,6 +989,13 @@ class MrpProduction(models.Model): productions_not_delivered.write( {'state': 'progress', 'programming_state': '已编程', 'is_rework': False}) + # 对制造订单所以面的cnc工单的程序用刀进行校验 + try: + logging.info(f'已更新制造订单:{productions_not_delivered}') + productions.production_cnc_tool_checkout() + except Exception as e: + logging.info(f'对cnc工单的程序用刀进行校验报错:{e}') + # 从cloud获取重新编程过的最新程序 def get_new_program(self, processing_panel): try: diff --git a/sf_manufacturing/models/mrp_routing_workcenter.py b/sf_manufacturing/models/mrp_routing_workcenter.py index 0c380ebd..a584379f 100644 --- a/sf_manufacturing/models/mrp_routing_workcenter.py +++ b/sf_manufacturing/models/mrp_routing_workcenter.py @@ -40,7 +40,7 @@ class ResMrpRoutingWorkcenter(models.Model): def get_company_id(self): self.company_id = self.env.user.company_id.id - company_id = fields.Many2one('res.company', compute="get_company_id", related=False) + company_id = fields.Many2one('res.company', compute="get_company_id", related=False, store=True) # 排产的时候, 根据坯料的长宽高比对一下机床的最大加工尺寸.不符合就不要分配给这个加工中心(机床). # 工单对应的工作中心,根据工序中的工作中心去匹配, diff --git a/sf_manufacturing/models/mrp_workcenter.py b/sf_manufacturing/models/mrp_workcenter.py index 34185d4e..27fdb37c 100644 --- a/sf_manufacturing/models/mrp_workcenter.py +++ b/sf_manufacturing/models/mrp_workcenter.py @@ -231,13 +231,13 @@ class ResWorkcenter(models.Model): default_capacity = round( self.production_line_hour_capacity * date_planned_working_hours, 2) _logger.info('排程日期:%s,计划数量:%s,日产能:%s,日工时:%s' % ( - date_planned, sum_qty, default_capacity, date_planned_working_hours)) + date_planned, sum_qty, default_capacity, date_planned_working_hours)) if sum_qty >= default_capacity: return False return True # 处理排程是否超过小时产能 - def deal_available_single_machine_capacity(self, date_planned): + def deal_available_single_machine_capacity(self, date_planned, count): date_planned_start = date_planned.strftime('%Y-%m-%d %H:00:00') date_planned_end = date_planned + timedelta(hours=1) @@ -249,7 +249,11 @@ class ResWorkcenter(models.Model): if plan_ids: sum_qty = sum([p.product_qty for p in plan_ids]) - if sum_qty >= self.production_line_hour_capacity: + production_line_hour_capacity = self.production_line_hour_capacity + if sum_qty >= production_line_hour_capacity: + message = '当前计划开始时间不能预约排程,超过生产线小时产能(%d件)%d件' % ( + production_line_hour_capacity, count) + raise UserError(message) return False return True diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py index 7887f08c..37d42b42 100644 --- a/sf_manufacturing/models/mrp_workorder.py +++ b/sf_manufacturing/models/mrp_workorder.py @@ -59,7 +59,8 @@ class ResMrpWorkOrder(models.Model): compute='_compute_state', store=True, default='pending', copy=False, readonly=True, recursive=True, index=True, tracking=True) - # state = fields.Selection(selection_add=[('to be detected', "待检测"), ('rework', '返工')], tracking=True) + delivery_warning = fields.Selection([('normal', '正常'), ('warning', '告警'), ('overdue', '逾期')], string='时效', + tracking=True) @api.depends('production_id.manual_quotation') def _compute_manual_quotation(self): @@ -225,6 +226,9 @@ class ResMrpWorkOrder(models.Model): material_height = fields.Float(string='高') # 零件图号 part_number = fields.Char(related='production_id.part_number', string='零件图号') + machining_drawings = fields.Binary('2D加工图纸', related='production_id.part_drawing', readonly=True) + quality_standard = fields.Binary('质检标准', related='production_id.quality_standard', readonly=True) + # 工序状态 process_state = fields.Selection([ ('待装夹', '待装夹'), @@ -302,6 +306,7 @@ class ResMrpWorkOrder(models.Model): is_delivery = fields.Boolean('是否配送完成', default=False) rfid_code = fields.Char('RFID码') rfid_code_old = fields.Char('RFID码(已解除)') + is_test_env = fields.Boolean('测试环境', default=False) production_line_id = fields.Many2one('sf.production.line', related='production_id.production_line_id', string='生产线', store=True, tracking=True) @@ -317,6 +322,9 @@ class ResMrpWorkOrder(models.Model): detailed_reason = fields.Text('详细原因') is_rework = fields.Boolean(string='是否返工', default=False) + def button_change_env(self): + self.is_test_env = not self.is_test_env + @api.constrains('blocked_by_workorder_ids') def _check_no_cyclic_dependencies(self): if self.production_id.state not in ['rework'] and self.state not in ['rework']: @@ -1048,6 +1056,17 @@ class ResMrpWorkOrder(models.Model): if workorder.production_id.tool_state in ['1', '2'] and workorder.state == 'ready': workorder.state = 'waiting' continue + if (workorder.production_id.tool_state in ['1', '2'] + and not workorder.production_id.workorder_ids.filtered(lambda a: a.sequence == 0) + and workorder.production_id.programming_state == '编程中' and workorder.name == '装夹预调'): + if workorder.state == 'pending' and workorder == self.search( + [('production_id', '=', workorder.production_id.id), + ('routing_type', '=', '装夹预调'), + ('state', 'not in', ['rework', 'done', 'cancel'])], + limit=1, + order="sequence"): + workorder.state = 'waiting' + continue # elif workorder.routing_type == 'CNC加工' and workorder.state not in ['done', 'cancel', 'progress', # 'rework']: @@ -1091,9 +1110,11 @@ class ResMrpWorkOrder(models.Model): if self.routing_type == '装夹预调': # 判断是否有坯料的序列号信息 boolean = False - if self.production_id.move_raw_ids[0].move_line_ids: - if self.production_id.move_raw_ids[0].move_line_ids[0].lot_id.name: - boolean = True + if self.production_id.move_raw_ids: + if self.production_id.move_raw_ids[0].move_line_ids: + if self.production_id.move_raw_ids[0].move_line_ids: + if self.production_id.move_raw_ids[0].move_line_ids[0].lot_id.name: + boolean = True if not boolean: raise UserError('制造订单【%s】缺少组件的序列号信息!' % self.production_id.name) self.pro_code = self.production_id.move_raw_ids[0].move_line_ids[0].lot_id.name @@ -1197,8 +1218,8 @@ class ResMrpWorkOrder(models.Model): if record.is_rework is False: if not record.material_center_point: raise UserError("坯料中心点为空,请检查") - if record.X_deviation_angle <= 0: - raise UserError("X偏差角度小于等于0,请检查!本次计算的X偏差角度为:%s" % record.X_deviation_angle) + # if record.X_deviation_angle <= 0: + # raise UserError("X偏差角度小于等于0,请检查!本次计算的X偏差角度为:%s" % record.X_deviation_angle) record.process_state = '待加工' # record.write({'process_state': '待加工'}) record.production_id.process_state = '待加工' @@ -1826,7 +1847,7 @@ class WorkPieceDelivery(models.Model): return is_free else: raise UserError("接驳站暂未反馈站点实时状态,请稍后再试") - + def delivery_avg(self): is_agv_task_dispatch = self.env['ir.config_parameter'].sudo().get_param('is_agv_task_dispatch') if is_agv_task_dispatch: diff --git a/sf_manufacturing/models/product_template.py b/sf_manufacturing/models/product_template.py index 7f4dc2b8..b7aa1adb 100644 --- a/sf_manufacturing/models/product_template.py +++ b/sf_manufacturing/models/product_template.py @@ -16,6 +16,12 @@ from OCC.Extend.DataExchange import write_stl_file class ResProductMo(models.Model): _inherit = 'product.template' + def _get_machining_precision(self): + machinings = self.env['sf.machining.accuracy'].sudo().search([]) + + list = [(m.sync_id, m.name) for m in machinings] + return list + model_file = fields.Binary('模型文件') categ_type = fields.Selection(string='产品的类别', related='categ_id.type', store=True) model_name = fields.Char('模型名称') @@ -23,12 +29,7 @@ class ResProductMo(models.Model): model_width = fields.Float('模型宽(mm)', digits=(16, 3)) model_height = fields.Float('模型高(mm)', digits=(16, 3)) model_volume = fields.Float('模型体积(m³)') - model_machining_precision = fields.Selection([ - ('0.10', '±0.10mm'), - ('0.05', '±0.05mm'), - ('0.03', '±0.03mm'), - ('0.02', '±0.02mm'), - ('0.01', '±0.01mm')], string='加工精度') + model_machining_precision = fields.Selection(selection=_get_machining_precision, string='加工精度') model_processing_panel = fields.Char('模型加工面板') model_remark = fields.Char('模型备注说明') length = fields.Float('长(mm)', digits=(16, 3)) @@ -774,6 +775,8 @@ class ResProductMo(models.Model): # bfm下单 manual_quotation = fields.Boolean('人工编程', default=False, readonly=True) part_number = fields.Char(string='零件图号', readonly=True) + machining_drawings = fields.Binary('2D加工图纸', readonly=True) + quality_standard = fields.Binary('质检标准', readonly=True) @api.constrains('tool_length') def _check_tool_length_size(self): @@ -835,6 +838,11 @@ class ResProductMo(models.Model): else: return self.env.ref('sf_dlm.product_uom_cubic_millimeter') + def attachment_update(self, name, res_id, res_field, mimetype): + attachment_info = self.env['ir.attachment'].sudo().search( + [('res_id', '=', res_id), ('res_field', '=', res_field)], limit=1) + attachment_info.write({'name': name, 'mimetype': mimetype}) + # 业务平台分配工厂后在智能工厂先创建销售订单再创建该产品 def product_create(self, product_id, item, order_id, order_number, i): copy_product_id = product_id.with_user(self.env.ref("base.user_admin")).copy() @@ -873,6 +881,9 @@ class ResProductMo(models.Model): 'manual_quotation': item['manual_quotation'] or False, 'part_number': item.get('part_number') or '', 'active': True, + 'machining_drawings': '' if not item['machining_drawings'] else base64.b64decode( + item['machining_drawings']), + 'quality_standard': '' if not item['quality_standard'] else base64.b64decode(item['quality_standard']), } tax_id = self.env['account.tax'].sudo().search( [('type_tax_use', '=', 'sale'), ('amount', '=', item.get('tax')), ('price_include', '=', 'True')]) @@ -880,6 +891,12 @@ class ResProductMo(models.Model): vals.update({'taxes_id': [(6, 0, [int(tax_id)])]}) copy_product_id.sudo().write(vals) product_id.product_tmpl_id.active = False + if item['machining_drawings'] and item['machining_drawings_name'] and item['machining_drawings_mimetype']: + self.attachment_update(item['machining_drawings_name'], copy_product_id.product_tmpl_id.id, + 'machining_drawings', item['machining_drawings_mimetype']) + if item['quality_standard'] and item['quality_standard_name'] and item['quality_standard_mimetype']: + self.attachment_update(item['quality_standard_name'], copy_product_id.product_tmpl_id.id, + 'quality_standard', item['quality_standard_mimetype']) return copy_product_id def _get_ids(self, param): diff --git a/sf_manufacturing/models/stock.py b/sf_manufacturing/models/stock.py index daf049eb..40d159d5 100644 --- a/sf_manufacturing/models/stock.py +++ b/sf_manufacturing/models/stock.py @@ -182,6 +182,11 @@ class StockRule(models.Model): moves._action_confirm() return True + def attachment_update(self, name, res_id, res_field): + attachment_info = self.env['ir.attachment'].sudo().search( + [('res_id', '=', res_id), ('res_field', '=', res_field)], limit=1) + attachment_info.write({'name': name}) + @api.model def _run_manufacture(self, procurements): productions_values_by_company = defaultdict(list) @@ -267,11 +272,6 @@ class StockRule(models.Model): workorder_duration += workorder.duration_expected sale_order = self.env['sale.order'].sudo().search([('name', '=', production.origin)]) - # 根据销售订单号查询快速订单 - quick_easy_order = self.env['quick.easy.order'].sudo().search([('sale_order_id', '=', sale_order.id)]) - if quick_easy_order: - production.write({'part_number': quick_easy_order.part_drawing_number, - 'part_drawing': quick_easy_order.machining_drawings}) if sale_order: # sale_order.write({'schedule_status': 'to schedule'}) self.env['sf.production.plan'].sudo().with_company(company_id).create({ diff --git a/sf_manufacturing/security/group_security.xml b/sf_manufacturing/security/group_security.xml index fdbc3ae5..040e7b92 100644 --- a/sf_manufacturing/security/group_security.xml +++ b/sf_manufacturing/security/group_security.xml @@ -1,5 +1,9 @@ - - + + + 演示模式 + + - \ No newline at end of file + + diff --git a/sf_manufacturing/security/ir.model.access.csv b/sf_manufacturing/security/ir.model.access.csv index fd3b0d21..be71cb0d 100644 --- a/sf_manufacturing/security/ir.model.access.csv +++ b/sf_manufacturing/security/ir.model.access.csv @@ -103,10 +103,20 @@ access_mrp_production_split_multi_group_sf_mrp_user,access.mrp.production.split. access_mrp_production_split_group_sf_mrp_user,access.mrp.production.split,mrp.model_mrp_production_split,sf_base.group_sf_mrp_user,1,1,1,0 access_mrp_production_split_line_group_sf_mrp_user,access.mrp.production.split.line,mrp.model_mrp_production_split_line,sf_base.group_sf_mrp_user,1,1,1,0 access_mrp_workcenter_capacity_manager_group_sf_mrp_user,mrp.workcenter.capacity.manager,mrp.model_mrp_workcenter_capacity,sf_base.group_sf_mrp_user,1,1,1,0 - - +access_mrp_workcenter_group_quality,mrp_workcenter_group_quality,model_mrp_workcenter,sf_base.group_quality,1,0,0,0 +access_mrp_workcenter_group_quality_director,mrp_workcenter_group_quality_director,model_mrp_workcenter,sf_base.group_quality_director,1,0,0,0 +access_sf_detection_result_group_quality,sf_detection_result_group_quality,model_sf_detection_result,sf_base.group_quality,1,0,1,0 +access_sf_detection_result_group_quality_director,sf_detection_result_group_quality_director,model_sf_detection_result,sf_base.group_quality_director,1,0,1,0 +access_mrp_workcenter_productivity_loss_group_quality,mrp_workcenter_productivity_loss_group_quality,mrp.model_mrp_workcenter_productivity_loss,sf_base.group_quality,1,0,0,0 +access_mrp_workcenter_productivity_loss_group_quality_director,mrp_workcenter_productivity_loss_group_quality_director,mrp.model_mrp_workcenter_productivity_loss,sf_base.group_quality_director,1,0,0,0 +access_mrp_workcenter_productivity_group_quality,mrp_workcenter_productivity_group_quality,mrp.model_mrp_workcenter_productivity,sf_base.group_quality,1,1,1,0 +access_mrp_workcenter_productivity_group_quality_director,mrp_workcenter_productivity_group_quality_director,mrp.model_mrp_workcenter_productivity,sf_base.group_quality_director,1,1,1,0 access_mrp_production_group_plan_dispatch,mrp_production,model_mrp_production,sf_base.group_plan_dispatch,1,1,0,0 -access_mrp_workorder,mrp_workorder,model_mrp_workorder,sf_base.group_plan_dispatch,1,1,1,0 +access_mrp_production_group_quality,mrp_production,model_mrp_production,sf_base.group_quality,1,1,0,0 +access_mrp_production_group_quality_director,mrp_production,model_mrp_production,sf_base.group_quality_director,1,1,0,0 +access_mrp_workorder_group_quality,mrp_workorder,model_mrp_workorder,sf_base.group_quality,1,1,0,0 +access_mrp_workorder_group_quality_director,mrp_workorder,model_mrp_workorder,sf_base.group_quality_director,1,1,0,0 +access_mrp_workorder,mrp_workorder,model_mrp_workorder,sf_base.group_plan_dispatch,1,1,0,0 access_sf_production_line_group_plan_dispatch,sf.production.line,model_sf_production_line,sf_base.group_plan_dispatch,1,0,0,0 access_sf_production_line_group_plan_director,sf.production.line,model_sf_production_line,sf_base.group_plan_director,1,1,1,0 access_sf_production_line,sf.production.line,model_sf_production_line,sf_maintenance.sf_group_equipment_user,1,1,1,0 diff --git a/sf_manufacturing/static/src/js/qr.js b/sf_manufacturing/static/src/js/qr.js new file mode 100644 index 00000000..fd501f27 --- /dev/null +++ b/sf_manufacturing/static/src/js/qr.js @@ -0,0 +1,115 @@ +/** @odoo-module **/ +import { registry } from '@web/core/registry'; +import { Component } from '@odoo/owl'; + +class QRCodeWidget extends Component { + // 初始化组件 + setup() { + console.log('QRCodeWidget setup'); + this.qrCodeValue = ''; // 初始化为空字符串,用于存储条码 + this.inputBuffer = ''; // 存储临时输入的字符 + this.inputTimer = null; // 定时器 + + // 显式绑定上下文 + this.onGlobalKeyDown = this.onGlobalKeyDown.bind(this); + window.addEventListener('keydown', this.onGlobalKeyDown); + } + + // 清理事件监听器,防止内存泄漏 + willUnmount() { + window.removeEventListener('keydown', this.onGlobalKeyDown); + if (this.inputTimer) { + clearTimeout(this.inputTimer); + } + } + + // 全局键盘事件监听器 + onGlobalKeyDown(event) { + + // 如果是Tab键,表示扫码输入结束 + if (event.key === 'Tab' || event.key === 'Enter') { + this.qrCodeValue = this.inputBuffer; // 完整条码赋值 + console.log('完整条码:', this.qrCodeValue); + this.onQRCodeChange(this.qrCodeValue); // 调用父组件的 onQRCodeChange 方法 + this.inputBuffer = ''; // 清空临时缓冲区 + event.preventDefault(); // 阻止Tab键的默认行为 + return; + } + + // 只处理可打印字符 + if (event.key.length === 1) { + this.inputBuffer += event.key; // 添加到缓冲区 + // console.log('当前缓冲区:', this.inputBuffer); + + // 清除之前的定时器,重新开始计时 + if (this.inputTimer) { + clearTimeout(this.inputTimer); + } + + // 启动一个定时器,如果500ms内没有新的输入,则认为条码输入完成 + this.inputTimer = setTimeout(() => { + this.qrCodeValue = this.inputBuffer; + // console.log('定时器触发,完整条码:', this.qrCodeValue); + this.inputBuffer = ''; // 清空缓冲区 + }, 500); // 可以根据需要调整时间 + } + + } + + // 处理二维码输入变更 + async onQRCodeChange(qrCodeValue) { + console.log('onQRCodeChange二维码输入变更', qrCodeValue); // 检查二维码的输入是否被捕获 + + if (qrCodeValue) { + // console.log('二维码输入变更'); + try { + // 发起 RPC 请求 + const result = await this.env.services.rpc('/web/dataset/call_kw', { + model: 'mrp.workorder', + method: 'search_read', + args: [ + [['rfid_code', '=', qrCodeValue]], // 查询条件 + ['id'] // 返回的字段 + ], + kwargs: {} + }); + + if (result.length > 0) { + console.log('该二维码对应的工单存在!'); + } else { + console.log('未找到对应的工单。'); + + const routingTypeField = document.querySelector('[name="routing_type"]'); + if (routingTypeField) { + let fieldValue = routingTypeField.querySelector('span').getAttribute('raw-value'); + console.log('Routing Type Value:', fieldValue); + // 清理多余的引号 + fieldValue = fieldValue ? fieldValue.replace(/["]+/g, '') : null; + console.log(fieldValue); + + if (fieldValue && fieldValue === '装夹预调') { + // console.log('routing_type 为装夹预调'); + + // 检查 RFID 值 + if (!qrCodeValue || qrCodeValue.length <= 3) return; + + // 查找 name="button_start" 按钮并触发点击事件 + const startButton = document.querySelector('[name="button_start"]'); + if (startButton) { + startButton.click(); + } + } + } + } + } catch (error) { + console.error('查询工单时出错:', error); + } + } + } + + // 返回模板名称 + static template = 'sf_manufacturing.QRCodeWidgetTemplate'; +} + +// 将自定义字段注册到字段注册表 +registry.category('fields').add('qrcode_widget', QRCodeWidget); diff --git a/sf_manufacturing/static/src/xml/qr.xml b/sf_manufacturing/static/src/xml/qr.xml new file mode 100644 index 00000000..415c0cd8 --- /dev/null +++ b/sf_manufacturing/static/src/xml/qr.xml @@ -0,0 +1,12 @@ + + + + + + +
+ +
+
+
+ diff --git a/sf_manufacturing/views/mrp_production_addional_change.xml b/sf_manufacturing/views/mrp_production_addional_change.xml index c6ae49ad..4c06c544 100644 --- a/sf_manufacturing/views/mrp_production_addional_change.xml +++ b/sf_manufacturing/views/mrp_production_addional_change.xml @@ -98,9 +98,9 @@ - + @@ -296,8 +296,11 @@ - - + + @@ -325,6 +328,16 @@ 投料状态 + + + + + + + + + + @@ -348,8 +361,11 @@ sequence + delivery_warning == 'warning' + delivery_warning == 'overdue' + - + +
+ 品牌: + +
+
规格: diff --git a/sf_manufacturing/views/mrp_workorder_view.xml b/sf_manufacturing/views/mrp_workorder_view.xml index d0577fbc..8d3c6c4f 100644 --- a/sf_manufacturing/views/mrp_workorder_view.xml +++ b/sf_manufacturing/views/mrp_workorder_view.xml @@ -101,7 +101,9 @@ current [('state', '!=', 'cancel'),('schedule_state', '=', '已排')] - {'search_default_product': 1, 'search_default_workcenter_id': active_id} + {'search_default_product': 1, 'search_default_workcenter_id': + active_id,'search_default_filter_order_warning':1,'search_default_filter_order_overdue':1,'search_default_filter_order_normal':1} +

没有工单要做! @@ -123,9 +125,9 @@ mrp.workorder - -