diff --git a/jikimo_account_process/__init__.py b/jikimo_account_process/__init__.py new file mode 100644 index 00000000..511a0ca3 --- /dev/null +++ b/jikimo_account_process/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import controllers +from . import models \ No newline at end of file diff --git a/jikimo_account_process/__manifest__.py b/jikimo_account_process/__manifest__.py new file mode 100644 index 00000000..f701deb7 --- /dev/null +++ b/jikimo_account_process/__manifest__.py @@ -0,0 +1,35 @@ +# -*- coding: utf-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 + """, + + 'author': "My Company", + 'website': "https://www.yourcompany.com", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/16.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Uncategorized', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['base', 'account'], + + # always loaded + 'data': [ + # 'security/ir.model.access.csv', + # 'views/views.xml', + # 'views/templates.xml', + ], + # only loaded in demonstration mode + 'demo': [ + # 'demo/demo.xml', + ], +} diff --git a/jikimo_account_process/controllers/__init__.py b/jikimo_account_process/controllers/__init__.py new file mode 100644 index 00000000..457bae27 --- /dev/null +++ b/jikimo_account_process/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/jikimo_account_process/controllers/controllers.py b/jikimo_account_process/controllers/controllers.py new file mode 100644 index 00000000..88f9332a --- /dev/null +++ b/jikimo_account_process/controllers/controllers.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# from odoo import http + + +# class JikimoAccountProcess(http.Controller): +# @http.route('/jikimo_account_process/jikimo_account_process', auth='public') +# def index(self, **kw): +# return "Hello, world" + +# @http.route('/jikimo_account_process/jikimo_account_process/objects', auth='public') +# def list(self, **kw): +# return http.request.render('jikimo_account_process.listing', { +# 'root': '/jikimo_account_process/jikimo_account_process', +# 'objects': http.request.env['jikimo_account_process.jikimo_account_process'].search([]), +# }) + +# @http.route('/jikimo_account_process/jikimo_account_process/objects/', auth='public') +# def object(self, obj, **kw): +# return http.request.render('jikimo_account_process.object', { +# 'object': obj +# }) diff --git a/jikimo_account_process/models/__init__.py b/jikimo_account_process/models/__init__.py new file mode 100644 index 00000000..bef2bae9 --- /dev/null +++ b/jikimo_account_process/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import account_move \ No newline at end of file diff --git a/jikimo_account_process/models/account_move.py b/jikimo_account_process/models/account_move.py new file mode 100644 index 00000000..e85d4e16 --- /dev/null +++ b/jikimo_account_process/models/account_move.py @@ -0,0 +1,15 @@ +from odoo import models, fields, api + +from odoo.exceptions import ValidationError + + +class CustomAccountMoveLine(models.Model): + _inherit = 'account.move' + _description = "account move line" + + @api.model_create_multi + def create(self, vals): + for val in vals: + val['name'] = self.env['ir.sequence'].next_by_code('account.move') or '/' + # 因为供应商与客户支付创建流程是先创建move line在修改来填充account_payment与move line的关联 + return super(CustomAccountMoveLine, self).create(vals) \ No newline at end of file diff --git a/jikimo_account_process/models/models.py b/jikimo_account_process/models/models.py new file mode 100644 index 00000000..ea14f4a4 --- /dev/null +++ b/jikimo_account_process/models/models.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +# from odoo import models, fields, api + + +# class jikimo_account_process(models.Model): +# _name = 'jikimo_account_process.jikimo_account_process' +# _description = 'jikimo_account_process.jikimo_account_process' + +# name = fields.Char() +# value = fields.Integer() +# value2 = fields.Float(compute="_value_pc", store=True) +# description = fields.Text() +# +# @api.depends('value') +# def _value_pc(self): +# for record in self: +# record.value2 = float(record.value) / 100 diff --git a/jikimo_frontend/static/src/js/custom_form_status_indicator.js b/jikimo_frontend/static/src/js/custom_form_status_indicator.js index 912c8efa..46e3ad70 100644 --- a/jikimo_frontend/static/src/js/custom_form_status_indicator.js +++ b/jikimo_frontend/static/src/js/custom_form_status_indicator.js @@ -53,6 +53,23 @@ const tableRequiredList = [ ] patch(FormStatusIndicator.prototype, 'jikimo_frontend.FormStatusIndicator', { + setup() { + owl.onMounted(() => { + try { + const dom = this.__owl__.bdom.el + const buttonsDom = $(dom).find('.o_form_status_indicator_buttons ') + if (buttonsDom) { + const dom1 = buttonsDom.children('.o_form_button_save') + const dom2 = buttonsDom.children('.o_form_button_cancel') + dom1.append('保存') + dom2.append('取消') + } + } catch (e) { + console.log(e) + } + + }); + }, // 你可以重写或者添加一些方法和属性 async _onDiscardChanges() { // var self = this; @@ -183,17 +200,6 @@ patch(ListRenderer.prototype, 'jikimo_frontend.ListRenderer', { // }) $(function () { - document.addEventListener('click', function () { - const dom = $('.o_form_status_indicator_buttons ') - if (dom) { - const dom1 = dom.children().eq(0) - const dom2 = dom.children().eq(1) - if (!dom1.text()) { - dom1.append('保存') - dom2.append('取消') - } - } - }) function customRequired() { let timer = null diff --git a/jikimo_hide_options/models/models.py b/jikimo_hide_options/models/models.py index 84aee828..8f30543c 100644 --- a/jikimo_hide_options/models/models.py +++ b/jikimo_hide_options/models/models.py @@ -324,4 +324,4 @@ def unlink(self): BaseModel._create = _create -BaseModel.unlink = unlink \ No newline at end of file +# BaseModel.unlink = unlink \ No newline at end of file diff --git a/sf_base/__manifest__.py b/sf_base/__manifest__.py index ceedcdd0..b10c2630 100644 --- a/sf_base/__manifest__.py +++ b/sf_base/__manifest__.py @@ -35,6 +35,7 @@ ], 'web.assets_backend': [ 'sf_base/static/src/scss/*.scss', + 'sf_base/static/src/js/*.js', ], }, diff --git a/sf_base/models/__init__.py b/sf_base/models/__init__.py index f7d50427..82ed80c1 100644 --- a/sf_base/models/__init__.py +++ b/sf_base/models/__init__.py @@ -5,3 +5,4 @@ from . import fixture from . import functional_fixture from . import tool_other_features from . import basic_parameters_fixture +from . import ir_sequence diff --git a/sf_base/models/basic_parameters_fixture.py b/sf_base/models/basic_parameters_fixture.py index e9469355..868114e8 100644 --- a/sf_base/models/basic_parameters_fixture.py +++ b/sf_base/models/basic_parameters_fixture.py @@ -13,7 +13,7 @@ class BasicParametersFixture(models.Model): diameter = fields.Float('直径(mm)', digits=(16, 2)) # '零点卡盘' 字段 - weight = fields.Float('重量(mm)', digits=(16, 2)) + weight = fields.Float('重量(kg)', digits=(16, 2)) orientation_dish_diameter = fields.Float('定位盘直径(mm)', digits=(16, 2)) clamping_diameter = fields.Float('装夹直径(mm)', digits=(16, 2)) clamping_num = fields.Selection([('1', '1'), ('2', '2'), ('4', '4'), ('6', '6'), ('8', '8')], string='装夹单元数') diff --git a/sf_base/models/common.py b/sf_base/models/common.py index 65dfe13d..95572631 100644 --- a/sf_base/models/common.py +++ b/sf_base/models/common.py @@ -84,10 +84,12 @@ class MrsProductionProcessCategory(models.Model): class MrsProductionProcess(models.Model): _name = 'sf.production.process' _description = '表面工艺' + order = 'sequence asc' code = fields.Char("编码") name = fields.Char('名称') remark = fields.Text("备注") + sequence = fields.Integer('排序') # processing_order_ids = fields.One2many('sf.processing.order', 'production_process_id', string='工序') partner_process_ids = fields.Many2many('res.partner', 'process_ids', '加工工厂') active = fields.Boolean('有效', default=True) @@ -96,7 +98,7 @@ class MrsProductionProcess(models.Model): # workcenter_ids = fields.Many2many('mrp.workcenter', 'rel_workcenter_process', required=True) processing_day = fields.Float('加工天数/d') travel_day = fields.Float('路途天数/d') - + sequence = fields.Integer('排序') # class MrsProcessingTechnology(models.Model): # _name = 'sf.processing.technology' @@ -148,6 +150,7 @@ class MrsProductionProcessParameter(models.Model): processing_day = fields.Float('加工天数/d') travel_day = fields.Float('路途天数/d') active = fields.Boolean('有效', default=True) + processing_mm = fields.Char('加工厚度/mm') def name_get(self): result = [] diff --git a/sf_base/models/ir_sequence.py b/sf_base/models/ir_sequence.py new file mode 100644 index 00000000..129728e5 --- /dev/null +++ b/sf_base/models/ir_sequence.py @@ -0,0 +1,74 @@ +import calendar +from datetime import timedelta + +from odoo import models, fields + + +class IrSequence(models.Model): + _inherit = 'ir.sequence' + + date_range_period = fields.Selection( + [('day', '每日'), ('month', '每月'), ('year', '每年')], + string='日期期间', + ) + + def _next(self, sequence_date=None): + """ Returns the next number in the preferred sequence in all the ones given in self.""" + if not self.use_date_range: + return self._next_do() + # date mode + dt = sequence_date or self._context.get('ir_sequence_date', fields.Date.today()) + seq_date = self.env['ir.sequence.date_range'].search( + [ + ('sequence_id', '=', self.id), + ('date_from', '<=', dt), + ('date_to', '>=', dt), + ('date_range_period', '=', self.date_range_period) + ], limit=1) + if not seq_date: + if self.date_range_period: + seq_date = self._create_date_range_seq_by_period(dt, self.date_range_period) + else: + seq_date = self._create_date_range_seq(dt) + return seq_date.with_context(ir_sequence_date_range=seq_date.date_from)._next() + + def _create_date_range_seq_by_period(self, date, period): + if period == 'year': + year = fields.Date.from_string(date).strftime('%Y') + date_from = '{}-01-01'.format(year) + date_to = '{}-12-31'.format(year) + if period == 'month': + # 计算当前月份的第一天和最后一天 + year = fields.Date.from_string(date).strftime('%Y') + month = fields.Date.from_string(date).strftime('%m') + date_from = fields.Date.from_string(date).strftime('%Y-%m-01') + date_to = '{}-{}-{}'.format(year, month, calendar.monthrange(int(year), int(month))[1]) + if period == 'day': + date_from = date + date_to = date + date_range = self.env['ir.sequence.date_range'].search( + [ + ('sequence_id', '=', self.id), + ('date_to', '>=', date_from), + ('date_to', '<=', date), + ('date_range_period', '=', period) + ], + order='date_to desc', limit=1) + if date_range: + date_from = date_range.date_to + timedelta(days=1) + seq_date_range = self.env['ir.sequence.date_range'].sudo().create({ + 'date_from': date_from, + 'date_to': date_to, + 'sequence_id': self.id, + 'date_range_period': period, + }) + return seq_date_range + + +class IrSequenceDateRange(models.Model): + _inherit = 'ir.sequence.date_range' + + date_range_period = fields.Selection( + [('day', '每日'), ('month', '每月'), ('year', '每年')], + string='日期期间', + ) diff --git a/sf_base/models/tool_base_new.py.rej b/sf_base/models/tool_base_new.py.rej deleted file mode 100644 index 6db1f28a..00000000 --- a/sf_base/models/tool_base_new.py.rej +++ /dev/null @@ -1,10 +0,0 @@ -diff a/sf_base/models/tool_base_new.py b/sf_base/models/tool_base_new.py (rejected hunks) -@@ -108,6 +108,4 @@ - cutting_speed_ids = fields.One2many('sf.cutting.speed', 'standard_library_id', string='切削速度Vc') -- feed_per_tooth_ids = fields.One2many('sf.feed.per.tooth', 'standard_library_id', '每齿走刀量fz', -- domain=[('cutting_speed', '!=', False)]) -- feed_per_tooth_ids_3 = fields.One2many('sf.feed.per.tooth', 'standard_library_id', '每齿走刀量fz', -- domain=[('cutting_speed', '!=', False)]) -+ feed_per_tooth_ids = fields.One2many('sf.feed.per.tooth', 'standard_library_id', '每齿走刀量fz') -+ feed_per_tooth_ids_3 = fields.One2many('sf.feed.per.tooth', 'standard_library_id', '每齿走刀量fz') - diff --git a/sf_base/static/src/js/remove_focus.js b/sf_base/static/src/js/remove_focus.js new file mode 100644 index 00000000..3e4cbb22 --- /dev/null +++ b/sf_base/static/src/js/remove_focus.js @@ -0,0 +1,23 @@ +/** @odoo-module **/ + +import { registry } from '@web/core/registry'; + +import { formView } from '@web/views/form/form_view'; +import { FormController } from '@web/views/form/form_controller'; + +import { onRendered, onMounted } from "@odoo/owl"; + +export class RemoveFocusController extends FormController { + setup() { + super.setup(); + + onMounted(() => { + this.__owl__.bdom.el.querySelectorAll(':focus').forEach(element => element.blur()); + }) + } +} + +registry.category('views').add('remove_focus_view', { + ...formView, + Controller: RemoveFocusController, +}); \ No newline at end of file diff --git a/sf_base/views/common_view.xml b/sf_base/views/common_view.xml index 533a3e04..c529354f 100644 --- a/sf_base/views/common_view.xml +++ b/sf_base/views/common_view.xml @@ -16,7 +16,7 @@ sf.production.process.parameter -
+

@@ -33,11 +33,12 @@ + - + @@ -52,7 +53,7 @@ - + @@ -140,7 +141,7 @@ sf.production.process.category - + @@ -163,7 +164,8 @@ sf.production.process - + + @@ -174,7 +176,7 @@ sf.production.process - +

@@ -192,11 +194,11 @@ - - - + + + - + diff --git a/sf_base/views/fixture_view.xml b/sf_base/views/fixture_view.xml index 0e86738d..07dadd33 100644 --- a/sf_base/views/fixture_view.xml +++ b/sf_base/views/fixture_view.xml @@ -171,7 +171,7 @@ - + @@ -197,7 +197,7 @@ - + @@ -220,7 +220,7 @@ - + @@ -248,7 +248,7 @@ - + @@ -278,7 +278,7 @@ - + @@ -307,7 +307,7 @@ - + @@ -335,7 +335,7 @@ - + diff --git a/sf_bf_connect/models/http.py b/sf_bf_connect/models/http.py index 8b10aa21..e70d5e79 100644 --- a/sf_bf_connect/models/http.py +++ b/sf_bf_connect/models/http.py @@ -36,7 +36,7 @@ class Http(models.AbstractModel): post_time = int(datas['HTTP_TIMESTAMP']) datetime_post = datetime.fromtimestamp(post_time) datetime_now = datetime.now().replace(microsecond=0) - datetime_del = datetime_now + timedelta(seconds=5) + datetime_del = datetime_now + timedelta(seconds=30) if datetime_post > datetime_del: raise AuthenticationError('请求已过期') check_str = '%s%s%s' % (datas['HTTP_TOKEN'], post_time, factory_secret.sf_secret_key) diff --git a/sf_bf_connect/models/process_status.py b/sf_bf_connect/models/process_status.py index 705aca4c..f4c4d859 100644 --- a/sf_bf_connect/models/process_status.py +++ b/sf_bf_connect/models/process_status.py @@ -1,7 +1,8 @@ from datetime import datetime import logging import requests -from odoo import fields, models +from odoo.exceptions import UserError +from odoo import fields, models, _ _logger = logging.getLogger(__name__) @@ -14,26 +15,49 @@ class StatusChange(models.Model): def action_confirm(self): # 在原有方法执行前记录日志和执行其他操作 logging.info('函数已经执行=============') + server_product_none = [] + for order in self.order_line: + gain_way_no = order.product_template_id.model_process_parameters_ids.filtered(lambda a: not a.gain_way) + if gain_way_no: + process_parameters = [item.name for item in gain_way_no] + raise UserError( + _("请先至【制造】-【配置】中【表面工艺可选参数】为【%s】填写获取方式", ", ".join(process_parameters))) + for item in order.product_template_id.model_process_parameters_ids: + if item.gain_way == '外协': + server_product = self.env['product.template'].search( + [('server_product_process_parameters_id', '=', item.id), + ('detailed_type', '=', 'service')]) + if not server_product: + server_product_none.append(item.name) + if server_product_none: + raise UserError(_("请先至【产品】中创建【表面工艺参数】为【%s】的服务产品", ", ".join(server_product_none))) # 使用super()来调用原始方法(在本例中为'sale.order'模型的'action_confirm'方法) - res = super(StatusChange, self).action_confirm() - - # 原有方法执行后,进行额外的操作(如调用外部API) - process_start_time = str(datetime.now()) - config = self.env['res.config.settings'].get_values() - json1 = { - 'params': { - 'model_name': 'jikimo.process.order', - 'field_name': 'name', - 'default_code': self.default_code, - 'state': '加工中', - 'process_start_time': process_start_time, - }, - } - url1 = config['bfm_url_new'] + '/api/get/state/get_order' - requests.post(url1, json=json1, data=None) - logging.info('接口已经执行=============') - + try: + res = super(StatusChange, self).action_confirm() + # 原有方法执行后,进行额外的操作(如调用外部API) + process_start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + config = self.env['res.config.settings'].get_values() + json1 = { + 'params': { + 'model_name': 'jikimo.process.order', + 'field_name': 'name', + 'default_code': self.default_code, + 'state': '加工中', + 'process_start_time': process_start_time, + }, + } + url1 = config['bfm_url_new'] + '/api/get/state/get_order' + ret = requests.post(url1, json=json1, data=None) + ret = ret.json() + if not ret.get('error'): + logging.info('接口已经执行=============') + else: + logging.error('工厂加工同步订单状态失败 {}'.format(ret.text)) + raise UserError('工厂加工同步订单状态失败') + except UserError as e: + logging.error('工厂加工同步订单状态失败 {}'.format(e)) + raise UserError('工厂加工同步订单状态失败') return res def action_cancel(self): @@ -202,12 +226,12 @@ class FinishStatusChange(models.Model): [('id', 'child_of', self.picking_type_id.warehouse_id.view_location_id.id), ('usage', '!=', 'supplier')]) if self.env['stock.move'].search([ - ('state', 'in', ['confirmed', 'partially_available', 'waiting', 'assigned']), - ('product_qty', '>', 0), - ('location_id', 'in', wh_location_ids), - ('move_orig_ids', '=', False), - ('picking_id', 'not in', self.ids), - ('product_id', 'in', lines.product_id.ids)], limit=1): + ('state', 'in', ['confirmed', 'partially_available', 'waiting', 'assigned']), + ('product_qty', '>', 0), + ('location_id', 'in', wh_location_ids), + ('move_orig_ids', '=', False), + ('picking_id', 'not in', self.ids), + ('product_id', 'in', lines.product_id.ids)], limit=1): action = self.action_view_reception_report() action['context'] = {'default_picking_ids': self.ids} return action diff --git a/sf_dlm/models/product_supplierinfo.py b/sf_dlm/models/product_supplierinfo.py index 05191b44..a46f4856 100644 --- a/sf_dlm/models/product_supplierinfo.py +++ b/sf_dlm/models/product_supplierinfo.py @@ -122,7 +122,7 @@ class ResMrpBomMo(models.Model): # 查bom的原材料 def get_raw_bom(self, product): raw_bom = self.env['product.product'].search( - [('categ_id.type', '=', '原材料'), ('materials_type_id', '=', product.materials_type_id.id)]) + [('categ_id.type', '=', '原材料'), ('materials_type_id', '=', product.materials_type_id.id)],limit=1) return raw_bom diff --git a/sf_hr/__init__.py b/sf_hr/__init__.py new file mode 100644 index 00000000..cde864ba --- /dev/null +++ b/sf_hr/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/sf_hr/__manifest__.py b/sf_hr/__manifest__.py new file mode 100644 index 00000000..aaf9cfc7 --- /dev/null +++ b/sf_hr/__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': ['hr'], + 'data': [ + 'views/hr_employee.xml', + ], + 'demo': [ + ], + 'qweb': [ + ], + 'license': 'LGPL-3', + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/sf_hr/models/__init__.py b/sf_hr/models/__init__.py new file mode 100644 index 00000000..633f8661 --- /dev/null +++ b/sf_hr/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- + diff --git a/sf_hr/security/ir.model.access.csv b/sf_hr/security/ir.model.access.csv new file mode 100644 index 00000000..e69de29b diff --git a/sf_hr/views/hr_employee.xml b/sf_hr/views/hr_employee.xml new file mode 100644 index 00000000..a3db9076 --- /dev/null +++ b/sf_hr/views/hr_employee.xml @@ -0,0 +1,15 @@ + + + + + employee_form + hr.employee + + + + 1 + + + + + \ No newline at end of file diff --git a/sf_machine_connect/__manifest__.py b/sf_machine_connect/__manifest__.py index db7ae467..ac8aef64 100644 --- a/sf_machine_connect/__manifest__.py +++ b/sf_machine_connect/__manifest__.py @@ -30,6 +30,7 @@ 'views/machine_info_present.xml', 'views/delivery_record.xml', 'views/res_config_settings_views.xml', + 'views/maintenance_views.xml', ], 'assets': { diff --git a/sf_machine_connect/controllers/controllers.py b/sf_machine_connect/controllers/controllers.py index ba8139d0..35f4a257 100644 --- a/sf_machine_connect/controllers/controllers.py +++ b/sf_machine_connect/controllers/controllers.py @@ -1,10 +1,55 @@ # -*- coding: utf-8 -*- +import re import ast import json +import base64 import logging +import psycopg2 +from datetime import datetime, timedelta from odoo import http from odoo.http import request +# 数据库连接配置 +db_config = { + "database": "timeseries_db", + "user": "postgres", + "password": "postgres", + "port": "5432", + "host": "172.16.10.98" +} + + +def convert_to_seconds(time_str): + # 修改正则表达式,使 H、M、S 部分可选 + + pattern = r"(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?" + match = re.match(pattern, time_str) + + if match: + # 提取各时间单位,如果某个单位缺失则默认设为0 + hours = int(match.group(1)) if match.group(1) else 0 + minutes = int(match.group(2)) if match.group(2) else 0 + seconds = int(match.group(3)) if match.group(3) else 0 + + # 计算总秒数 + total_seconds = hours * 3600 + minutes * 60 + seconds + if total_seconds == 0: + # return None + pattern = r"(?:(\d+)小时)?(?:(\d+)分钟)?(?:(\d+)秒)?" + match = re.match(pattern, time_str) + if match: + # 提取各时间单位,如果某个单位缺失则默认设为0 + hours = int(match.group(1)) if match.group(1) else 0 + minutes = int(match.group(2)) if match.group(2) else 0 + seconds = int(match.group(3)) if match.group(3) else 0 + + # 计算总秒数 + total_seconds = hours * 3600 + minutes * 60 + seconds + return total_seconds + else: + return None + return total_seconds + class Sf_Dashboard_Connect(http.Controller): @@ -18,6 +63,11 @@ class Sf_Dashboard_Connect(http.Controller): """ res = {'status': 1, 'message': '成功', 'data': []} logging.info('前端请求机床数据的参数为:%s' % kw) + + # 获取当前时间的时间戳 + current_timestamp = datetime.now().timestamp() + print(current_timestamp) + # tem_list = [ # "XT-GNJC-WZZX-X800-Y550-Z550-T24-A5-1", "XT-GNJC-LSZX-X800-Y550-Z550-T24-A3-3", # "XT-GNJC-LSZX-X800-Y550-Z550-T24-A3-4", "XT-GNJC-LSZX-X800-Y550-Z550-T24-A3-5", @@ -33,10 +83,24 @@ class Sf_Dashboard_Connect(http.Controller): machine_list = ast.literal_eval(kw['machine_list']) for item in machine_list: machine_data = equipment_obj.search([('code', '=', item)]) + + # 机床上线时间段 + first_online_duration = current_timestamp - int(machine_data.first_online_time.timestamp()) + + power_off_time = None + power_off_rate = None + if machine_data.machine_power_on_time: + power_off_time = first_online_duration - convert_to_seconds(machine_data.machine_power_on_time) + power_off_rate = round((power_off_time / first_online_duration), 3) + else: + power_off_time = False + power_off_rate = False if machine_data: res['data'].append({ + 'active': machine_data.status, 'id': machine_data.id, 'name': machine_data.name, + 'brand': machine_data.type_id.name, 'code': machine_data.code, 'status': machine_data.status, 'run_status': machine_data.run_status, @@ -88,11 +152,681 @@ class Sf_Dashboard_Connect(http.Controller): 'alarm_time': machine_data.alarm_time, 'alarm_msg': machine_data.alarm_msg, 'clear_time': machine_data.clear_time, + # 计算出来的数据 + # 开动率:运行时间/通电时间 + 'run_rate': machine_data.run_rate, + # 关机时长:初次上线时间 - 通电时间 + 'power_off_time': power_off_time, + # 关机率:关机时长/初次上线时间 + 'power_off_rate': power_off_rate, + 'first_online_duration': first_online_duration, + # 停机时间:关机时间 - 运行时间 + # 停机时长:关机时间 - 初次上线时间 + 'img': f'data:image/png;base64,{machine_data.machine_tool_picture.decode("utf-8")}', + 'equipment_type': machine_data.category_id.name, }) - return json.JSONEncoder().encode(res) + return json.dumps(res) except Exception as e: logging.info('前端请求机床数据失败,原因:%s' % e) res['status'] = -1 res['message'] = '前端请求机床数据失败,原因:%s' % e return json.JSONEncoder().encode(res) + + @http.route('/api/logs/list', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def logs_list(self, **kw): + """ + 拿到日志数据返回给大屏展示 + :param kw: + :return: + """ + res = {'status': 1, 'message': '成功', 'data': {}} + logging.info('前端请求日志数据的参数为:%s' % kw) + + try: + # 连接数据库 + conn = psycopg2.connect(**db_config) + cur = conn.cursor() + machine_list = ast.literal_eval(kw['machine_list']) + begin_time_str = kw['begin_time'].strip('"') + end_time_str = kw['end_time'].strip('"') + + begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') + + print('begin_time: %s' % begin_time) + + for item in machine_list: + sql = ''' + SELECT time, device_state, program_name + FROM device_data + WHERE device_name = %s AND time >= %s AND time <= %s + ORDER BY time DESC; + ''' + # 执行SQL命令,使用参数绑定 + cur.execute(sql, (item, begin_time, end_time)) + results = cur.fetchall() + + # 将数据按照 equipment_code 进行分组 + if item not in res['data']: + res['data'][item] = [] + + for result in results: + res['data'][item].append({ + 'time': result[0].strftime('%Y-%m-%d %H:%M:%S'), + 'state': result[1], + 'production_name': result[2], + }) + + return json.dumps(res) # 注意使用 json.dumps 而不是直接用 json.JSONEncoder().encode() + + except Exception as e: + logging.info('前端请求日志数据失败,原因:%s' % e) + res['status'] = -1 + res['message'] = '前端请求日志数据失败,原因:%s' % e + return json.dumps(res) + + # 返回CNC机床列表 + @http.route('/api/CNCList', type='http', auth='public', methods=['GET', 'POST'], csrf=False, + cors="*") + def CNCList(self, **kw): + """ + 获取CNC机床列表 + :param kw: + :return: + """ + + # logging.info('CNCList:%s' % kw) + try: + res = {'Succeed': True} + # cnc_list = request.env['sf.cnc.equipment'].sudo().search([]) + # cnc_list = ["XT-GNJC-WZZX-X800-Y550-Z550-T24-A5-1", "XT-GNJC-LSZX-X800-Y550-Z550-T24-A3-3", + # "XT-GNJC-LSZX-X800-Y550-Z550-T24-A3-4", "XT-GNJC-LSZX-X800-Y550-Z550-T24-A3-5", + # "XT-GNJC-LSZX-X800-Y550-Z550-T24-A3-6", "XT-GNJC-LSZX-X800-Y550-Z550-T24-A3-7", + # "XT-GNJC-LSZX-X800-Y550-Z550-T24-A3-8", "XT-GNJC-WZZX-X800-Y550-Z550-T24-A5-2", + # "XT-GNJC-GSZG-X600-Y400-Z350-T21-A3-9", "XT-GNJC-GSZG-X600-Y400-Z350-T21-A3-10", + # "XT-GNJC-GSZG-X600-Y400-Z350-T21-A3-11", "XT-GNJC-GSZG-X600-Y400-Z350-T21-A3-12", + # "XT-GNJC-GSZG-X600-Y400-Z350-T21-A3-13", "XT-GNJC-GSZG-X600-Y400-Z350-T21-A3-14"] + + cnc_list_obj = request.env['maintenance.equipment'].sudo().search( + [('function_type', '!=', False), ('active', '=', True)]) + cnc_list = list(map(lambda x: x.code, cnc_list_obj)) + print('cnc_list: %s' % cnc_list) + res['CNCList'] = cnc_list + + except Exception as e: + res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} + logging.info('CNCList error:%s' % e) + return json.JSONEncoder().encode(res) + + # 返回产线列表 + + @http.route('/api/LineList', type='http', auth='public', methods=['GET', 'POST'], csrf=False, + + cors="*") + def LineList(self, **kw): + """ + 获取产线列表 + :param kw: + :return: + """ + + try: + res = {'Succeed': True} + line_list_obj = request.env['sf.production.line'].sudo().search([('name', 'ilike', 'CNC')]) + line_list = list(map(lambda x: x.name, line_list_obj)) + print('line_list: %s' % line_list) + res['LineList'] = line_list + + except Exception as e: + res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} + logging.info('LineList error:%s' % e) + + return json.JSONEncoder().encode(res) + + # 获取产线产量相关 + + @http.route('/api/LineProduct', type='http', auth='public', methods=['GET', 'POST'], csrf=False, + + cors="*") + def LineProduct(self, **kw): + """ + 获取产线产量相关 + :param kw: + :return: + """ + res = {'status': 1, 'message': '成功', 'data': {}} + logging.info('前端请求产线产量数据的参数为:%s' % kw) + + try: + plan_obj = request.env['sf.production.plan'].sudo() + line_list = ast.literal_eval(kw['line_list']) + print('line_list: %s' % line_list) + for line in line_list: + plan_data = plan_obj.search([('production_line_id.name', '=', line)]) + # 工单总量 + plan_data_total_counts = plan_obj.search_count([('production_line_id.name', '=', line)]) + # 工单完成量 + plan_data_finish_counts = plan_obj.search_count( + [('production_line_id.name', '=', line), ('state', 'in', ['finished'])]) + # 工单计划量 + plan_data_plan_counts = plan_obj.search_count( + [('production_line_id.name', '=', line), ('state', 'not in', ['finished'])]) + # 工单不良累计 + plan_data_fault_counts = plan_obj.search_count( + [('production_line_id.name', '=', line), ('production_id.state', 'in', ['scrap', 'cancel'])]) + + # 工单返工数量 + + plan_data_rework_counts = plan_obj.search_count( + [('production_line_id.name', '=', line), ('production_id.state', 'in', ['rework'])]) + + # 工单完成率 + finishe_rate = round( + (plan_data_finish_counts / plan_data_total_counts if plan_data_total_counts > 0 else 0), 3) + + # 工单进度偏差 + plan_data_progress_deviation = plan_data_finish_counts - plan_data_plan_counts + + if plan_data: + data = { + 'plan_data_total_counts': plan_data_total_counts, + 'plan_data_finish_counts': plan_data_finish_counts, + 'plan_data_plan_counts': plan_data_plan_counts, + 'plan_data_fault_counts': plan_data_fault_counts, + 'finishe_rate': finishe_rate, + 'plan_data_progress_deviation': plan_data_progress_deviation, + 'plan_data_rework_counts': plan_data_rework_counts + } + res['data'][line] = data + + return json.dumps(res) # 注意使用 json.dumps 而不是直接用 json.JSONEncoder().encode() + + except Exception as e: + logging.info('前端请求产线产量数据失败,原因:%s' % e) + res['status'] = -1 + res['message'] = '前端请求产线产量数据失败,原因:%s' % e + return json.dumps(res) + + # 日完成量统计 + @http.route('/api/DailyFinishCount', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def DailyFinishCount(self, **kw): + """ + 获取日完成量统计 + :param kw: + :return: + """ + res = {'status': 1, 'message': '成功', 'data': {}} + plan_obj = request.env['sf.production.plan'].sudo() + line_list = ast.literal_eval(kw['line_list']) + begin_time_str = kw['begin_time'].strip('"') + end_time_str = kw['end_time'].strip('"') + begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') + print('line_list: %s' % line_list) + + def get_date_list(start_date, end_date): + date_list = [] + current_date = start_date + while current_date <= end_date: + date_list.append(current_date) + current_date += timedelta(days=1) + return date_list + + for line in line_list: + date_list = get_date_list(begin_time, end_time) + order_counts = [] + + date_field_name = 'actual_end_time' # 替换为你模型中的实际字段名 + + for date in date_list: + next_day = date + timedelta(days=1) + orders = plan_obj.search([('production_line_id.name', '=', line), ('state', 'in', ['finished']), + (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), + (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) + ]) + + rework_orders = plan_obj.search( + [('production_line_id.name', '=', line), ('production_id.state', 'in', ['rework']), + (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), + (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) + ]) + not_passed_orders = plan_obj.search( + [('production_line_id.name', '=', line), ('production_id.state', 'in', ['scrap', 'cancel']), + (date_field_name, '>=', date.strftime('%Y-%m-%d 00:00:00')), + (date_field_name, '<', next_day.strftime('%Y-%m-%d 00:00:00')) + ]) + order_counts.append({ + 'date': date.strftime('%Y-%m-%d'), + 'order_count': len(orders), + 'rework_orders': len(rework_orders), + 'not_passed_orders': len(not_passed_orders) + }) + # 外面包一层,没什么是包一层不能解决的,包一层就能区分了,类似于包一层div + # 外面包一层的好处是,可以把多个数据结构打包在一起,方便前端处理 + + # date_list_dict = {line: order_counts} + + res['data'][line] = order_counts + return json.dumps(res) + + # 实时产量 + @http.route('/api/RealTimeProduct', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def RealTimeProduct(self, **kw): + """ + 获取实时产量 + :param kw: + :return: + """ + res = {'status': 1, 'message': '成功', 'data': {}} + plan_obj = request.env['sf.production.plan'].sudo() + line_list = ast.literal_eval(kw['line_list']) + begin_time_str = kw['begin_time'].strip('"') + end_time_str = kw['end_time'].strip('"') + begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') + + def get_hourly_intervals(start_time, end_time): + intervals = [] + current_time = start_time + while current_time < end_time: + next_hour = current_time + timedelta(hours=1) + intervals.append((current_time, min(next_hour, end_time))) + current_time = next_hour + return intervals + + # 当班计划量 + for line in line_list: + plan_order_nums = plan_obj.search_count( + [('production_line_id.name', '=', line), ('state', 'not in', ['draft']), + ('date_planned_start', '>=', begin_time), + ('date_planned_start', '<', end_time) + ]) + finish_order_nums = plan_obj.search_count( + [('production_line_id.name', '=', line), ('state', 'in', ['finished']), + ('date_planned_start', '>=', begin_time), + ('date_planned_start', '<', end_time) + ]) + hourly_intervals = get_hourly_intervals(begin_time, end_time) + production_counts = [] + + for start, end in hourly_intervals: + orders = plan_obj.search([ + ('actual_end_time', '>=', start.strftime('%Y-%m-%d %H:%M:%S')), + ('actual_end_time', '<', end.strftime('%Y-%m-%d %H:%M:%S')), + ('production_line_id.name', '=', line) + ]) + production_counts.append({ + 'start_time': start.strftime('%Y-%m-%d %H:%M:%S'), + 'end_time': end.strftime('%Y-%m-%d %H:%M:%S'), + 'production_count': len(orders) + }) + production_counts_dict = {'production_counts': production_counts, + 'plan_order_nums': plan_order_nums, + 'finish_order_nums': finish_order_nums, + } + + res['data'][line] = production_counts_dict + # res['data'].append({line: production_counts}) + return json.dumps(res) + + # 工单明细 + @http.route('/api/OrderDetail', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def OrderDetail(self, **kw): + """ + 获取工单明细 + :param kw: + :return: + """ + + # res = {'status': 1, 'message': '成功', 'not_done_data': [], 'done_data': []} + res = {'status': 1, 'message': '成功', 'data': {}} + plan_obj = request.env['sf.production.plan'].sudo() + line_list = ast.literal_eval(kw['line_list']) + begin_time_str = kw['begin_time'].strip('"') + end_time_str = kw['end_time'].strip('"') + begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') + print('line_list: %s' % line_list) + not_done_data = [] + done_data = [] + final_data = {} + + for line in line_list: + # 未完成订单 + not_done_orders = plan_obj.search( + [('production_line_id.name', '=', line), ('state', 'not in', ['finished'])]) + print(not_done_orders) + # 完成订单 + finish_orders = plan_obj.search([('production_line_id.name', '=', line), ('state', 'in', ['finished'])]) + print(finish_orders) + + # 获取所有未完成订单的ID列表 + order_ids = [order.id for order in not_done_orders] + # 获取所有已完成订单的ID列表 + finish_order_ids = [order.id for order in finish_orders] + + # 对ID进行排序 + sorted_order_ids = sorted(order_ids) + + finish_sorted_order_ids = sorted(finish_order_ids) + + # 创建ID与序号的对应关系 + id_to_sequence = {order_id: index + 1 for index, order_id in enumerate(sorted_order_ids)} + + finish_id_to_sequence = {order_id: index + 1 for index, order_id in enumerate(finish_sorted_order_ids)} + + # # 输出结果或进一步处理 + # for order_id, sequence in id_to_sequence.items(): + # print(f"Order ID: {order_id} - Sequence: {sequence}") + + for order in not_done_orders: + blank_name = '' + try: + blank_name = order.production_id.move_raw_ids[0].product_id.name + except: + continue + # blank_name = 'R-S00109-1 [碳素结构钢 Q235-118.0 * 72.0 * 21.0]' + # 正则表达式 + material_pattern = r'\[(.*?)-' # 从 [ 开始,碰到 - 停止 + dimensions = blank_name.split('-')[-1].split(']')[0] + + # 匹配材料名称 + material_match = re.search(material_pattern, blank_name) + material = material_match.group(1) if material_match else 'No match found' + + state_dict = { + 'draft': '待排程', + 'done': '已排程', + 'processing': '生产中', + 'finished': '已完成' + } + + line_dict = { + 'sequence': id_to_sequence[order.id], + 'workorder_name': order.name, + 'blank_name': blank_name, + 'material': material, + 'dimensions': dimensions, + 'order_qty': order.product_qty, + 'state': state_dict[order.state], + + } + not_done_data.append(line_dict) + + for finish_order in finish_orders: + blank_name = '' + try: + blank_name = finish_order.production_id.move_raw_ids[0].product_id.name + except: + continue + + material_pattern = r'\[(.*?)-' # 从 [ 开始,碰到 - 停止 + dimensions = blank_name.split('-')[-1].split(']')[0] + + # 匹配材料名称 + material_match = re.search(material_pattern, blank_name) + material = material_match.group(1) if material_match else 'No match found' + + line_dict = { + 'sequence': finish_id_to_sequence[finish_order.id], + 'workorder_name': finish_order.name, + 'blank_name': blank_name, + 'material': material, + 'dimensions': dimensions, + 'order_qty': finish_order.product_qty, + 'finish_time': finish_order.actual_end_time.strftime('%Y-%m-%d %H:%M:%S'), + + } + done_data.append(line_dict) + + # 开始包一层 + res['data'][line] = {'not_done_data': not_done_data, 'done_data': done_data} + return json.dumps(res) + + # 查询pg库来获得待机次数 + @http.route('/api/IdleAlarmCount', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def idle_alarm_count(self, **kw): + """ + 查询设备的待机次数 + """ + res = {'status': 1, 'message': '成功', 'data': {}} + logging.info('前端请求机床数据的参数为:%s' % kw) + + # 连接数据库 + conn = psycopg2.connect(**db_config) + cur = conn.cursor() + try: + # 获取请求的机床数据 + machine_list = ast.literal_eval(kw['machine_list']) + total_alarm_time = 0 + alarm_count_num = 0 + for item in machine_list: + sql = ''' + SELECT COUNT(*) + FROM ( + SELECT DISTINCT ON (idle_start_time) idle_start_time + FROM device_data + WHERE device_name = %s AND idle_start_time IS NOT NULL + ORDER BY idle_start_time, time + ) subquery; + ''' + + sql2 = ''' + SELECT DISTINCT ON (alarm_time) alarm_time, alarm_repair_time + FROM device_data + WHERE device_name = %s AND alarm_time IS NOT NULL + ORDER BY alarm_time, time; + + ''' + # 执行SQL命令 + cur.execute(sql, (item,)) + result = cur.fetchall() + print('result========', result) + + cur.execute(sql2, (item,)) + result2 = cur.fetchall() + print('result2========', result2) + # + for row in result: + res['data'][item] = {'idle_count': row[0]} + alarm_count = [] + for row in result2: + alarm_count.append(row[0]) + total_alarm_time += abs(float(row[0])) + if len(list(set(alarm_count))) == 1: + if list(set(alarm_count))[0] is None: + alarm_count_num = 0 + else: + alarm_count_num = 1 + else: + alarm_count_num = len(list(set(alarm_count))) + res['data'][item]['total_alarm_time'] = total_alarm_time / 3600 + res['data'][item]['alarm_count_num'] = alarm_count_num + + # 返回统计结果 + return json.dumps(res) + except Exception as e: + print(f"An error occurred: {e}") + return json.dumps(res) + finally: + cur.close() + conn.close() + + # 查询pg库来获得异常情况 + @http.route('/api/alarm/logs', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def alarm_logs(self, **kw): + """ + 查询设备的异常情况 + """ + res = {'status': 1, 'message': '成功', 'data': {}} + logging.info('前端请求机床数据的参数为:%s' % kw) + + # 连接数据库 + conn = psycopg2.connect(**db_config) + cur = conn.cursor() + try: + # 获取请求的机床数据 + # machine_list = ast.literal_eval(kw['machine_list']) + # idle_times = [] + # idle_dict = {} + + # for item in machine_list: + sql = ''' + SELECT DISTINCT ON (alarm_time) alarm_time, alarm_message, system_date, system_time, alarm_repair_time + FROM device_data + WHERE alarm_time IS NOT NULL + ORDER BY alarm_time, time; + + ''' + # 执行SQL命令 + cur.execute(sql) + result = cur.fetchall() + print('result', result) + + # 将查询结果转换为字典列表 + data = [] + for row in result: + record = { + 'alarm_time': row[0], + 'alarm_message': row[1], + 'system_date': row[2], + 'system_time': row[3], + 'alarm_repair_time': row[4] + } + data.append(record) + + # 将数据填充到返回结果中 + res['data'] = data + + # 返回统计结果 + return json.dumps(res, ensure_ascii=False) + except Exception as e: + print(f"An error occurred: {e}") + return json.dumps(res) + finally: + cur.close() + conn.close() + + # 设备oee + @http.route('/api/OEE', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def OEE(self, **kw): + """ + 获取产线等oee + """ + res = {'status': 1, 'message': '成功', 'data': {}} + logging.info('前端请求oee数据的参数为:%s' % kw) + + try: + count_oee = 1 + workcenter_obj = request.env['mrp.workcenter'].sudo() + workcenter_list = ast.literal_eval(kw['workcenter_list']) + print('workcenter_list: %s' % workcenter_list) + for line in workcenter_list: + res['data'][line] = workcenter_obj.search([('name', '=', line)]).oee + count_oee *= workcenter_obj.search([('name', '=', line)]).oee + res['data']['综合oee'] = count_oee / 1000000 + except Exception as e: + print(f"An error occurred: {e}") + + return json.dumps(res) + + # # 查询某段时间的设备oee + # @http.route('/api/OEEByTime', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + # def OEEByTime(self, **kw): + # """ + # 获取某段时间的oee + # """ + # res = {'status': 1, 'message': '成功', 'data': {}} + # logging.info('前端请求获取某段时间的oee的参数为:%s' % kw) + # workcenter_list = ast.literal_eval(kw['workcenter_list']) + # begin_time_str = kw['begin_time'].strip('"') + # end_time_str = kw['end_time'].strip('"') + # begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') + # end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') + # print('workcenter_list: %s' % workcenter_list) + # # 连接数据库 + # conn = psycopg2.connect(**db_config) + # cur = conn.cursor() + # # 查询并计算OEE平均值 + # oee_data = {} + # for workcenter in workcenter_list: + # cur.execute(""" + # SELECT AVG(oee) as avg_oee + # FROM oee_data + # WHERE workcenter_name = %s + # AND time BETWEEN %s AND %s + # """, (workcenter, begin_time, end_time)) + # + # result = cur.fetchone() + # avg_oee = result[0] if result else 0.0 + # oee_data[workcenter] = avg_oee + # + # # 返回数据 + # res['data'] = oee_data + # return json.dumps(res) + + @http.route('/api/OEEByTime', type='http', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + def OEEByTime(self, **kw): + """ + 获取某段时间的OEE,根据用户指定的时间单位(day或hour)返回对应的平均值。 + 如果不传time_unit,则默认按天返回,并补全没有数据的时间段,填充0值。 + """ + res = {'status': 1, 'message': '成功', 'data': {}} + logging.info('前端请求获取某段时间的OEE的参数为:%s' % kw) + + # 获取并解析参数 + workcenter_list = ast.literal_eval(kw['workcenter_list']) + begin_time_str = kw['begin_time'].strip('"') + end_time_str = kw['end_time'].strip('"') + time_unit = kw.get('time_unit', 'day') # 默认单位为天 + begin_time = datetime.strptime(begin_time_str, '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S') + + # 连接数据库 + conn = psycopg2.connect(**db_config) + cur = conn.cursor() + + # 根据时间单位选择不同的时间格式 + if time_unit == 'hour': + time_format = 'YYYY-MM-DD HH24:00:00' + time_delta = timedelta(hours=1) + else: # 默认为'day' + time_format = 'YYYY-MM-DD' + time_delta = timedelta(days=1) + + # 查询并计算OEE平均值 + oee_data = {} + for workcenter in workcenter_list: + cur.execute(f""" + SELECT to_char(time, '{time_format}') as time_unit, AVG(oee) as avg_oee + FROM oee_data + WHERE workcenter_name = %s + AND time BETWEEN %s AND %s + GROUP BY time_unit + ORDER BY time_unit + """, (workcenter, begin_time, end_time)) + + results = cur.fetchall() + # 初始化当前产线的OEE数据字典 + workcenter_oee = {row[0]: row[1] for row in results} + + # 补全缺失的时间段 + current_time = begin_time + if time_unit != 'hour': + while current_time <= end_time: + time_key = current_time.strftime('%Y-%m-%d') + if time_key not in workcenter_oee: + workcenter_oee[time_key] = 0 + current_time += time_delta + + # 按时间排序 + oee_data[workcenter] = dict(sorted(workcenter_oee.items())) + + # 关闭数据库连接 + cur.close() + conn.close() + + # 返回数据 + res['data'] = oee_data + return json.dumps(res) diff --git a/sf_machine_connect/models/__init__.py b/sf_machine_connect/models/__init__.py index ef61fd83..01ae60ae 100644 --- a/sf_machine_connect/models/__init__.py +++ b/sf_machine_connect/models/__init__.py @@ -2,3 +2,4 @@ from . import ftp_client from . import ftp_operate from . import py2opcua from . import res_config_setting +from . import mrp_workorder diff --git a/sf_machine_connect/models/ftp_client.py b/sf_machine_connect/models/ftp_client.py index 1e09f665..1d56424f 100644 --- a/sf_machine_connect/models/ftp_client.py +++ b/sf_machine_connect/models/ftp_client.py @@ -121,6 +121,13 @@ class Machine_ftp(models.Model): """ _inherit = 'maintenance.equipment' + # 机床首次上线时间(默认取值2024年08月01日零点) + + def _get_default_online_time(self): + return datetime(2024, 1, 1, 0, 0, 0) + + first_online_time = fields.Datetime(string='首次上线时间', default=_get_default_online_time) + # workorder_ids = fields.One2many('mrp.workorder', 'machine_tool_id', string='工单') # # 机床配置项目 @@ -275,7 +282,28 @@ class Machine_ftp(models.Model): alarm_msg = fields.Char('故障报警信息', readonly=True) clear_time = fields.Char('故障消除时间(复原时间)', readonly=True) - # 当前程序名, 机床累计运行时间, 机床系统日期, 机床系统时间, 当前刀具号, 机床循环时间 + # # 开动率 + run_rate = fields.Char('开动率', readonly=True) + + # 同步CNC设备到oee + def sync_oee(self): + """ + 同步CNC设备到oee + :return: + """ + for record in self: + record.ensure_one() + cnc_oee_dict = { + 'equipment_id': record.id, + 'type_id': record.type_id.id, + 'machine_tool_picture': record.machine_tool_picture, + 'equipment_code': record.code, + 'function_type': record.function_type, + } + if self.env['maintenance.equipment.oee.logs'].search([('equipment_id', '=', record.id)]): + self.env['maintenance.equipment.oee.logs'].write(cnc_oee_dict) + else: + self.env['maintenance.equipment.oee.logs'].create(cnc_oee_dict) class WorkCenterBarcode(models.Model): diff --git a/sf_machine_connect/models/mrp_workorder.py b/sf_machine_connect/models/mrp_workorder.py new file mode 100644 index 00000000..21cc96ac --- /dev/null +++ b/sf_machine_connect/models/mrp_workorder.py @@ -0,0 +1,38 @@ +import re + +from odoo import fields, models, api + + +class ResMrpWorkOrder(models.Model): + _inherit = 'mrp.workorder' + + mixed_search_field = fields.Char(string='坯料产品名称/RFID') + + @api.model + def web_read_group(self, domain, fields, groupby, limit=None, offset=0, orderby=False, + lazy=True, expand=False, expand_limit=None, expand_orderby=False): + domain = domain or [] + for index, item in enumerate(domain): + if isinstance(item, list): + if item[0] == 'mixed_search_field': + if self._is_rfid_code(item[2]): + domain[index] = ['rfid_code', item[1], item[2]] + else: + domain[index] = ['product_tmpl_name', item[1], item[2]] + + return super(ResMrpWorkOrder, self).web_read_group(domain, fields, groupby, limit=limit, offset=offset, orderby=orderby, + lazy=lazy, expand=expand, expand_limit=expand_limit, expand_orderby=expand_orderby) + + def _is_rfid_code(self, tag): + """ + 判断是否是rfid_code + """ + # 基于长度判断(假设RFID标签长度为10到16个字符) + if not 10 <= len(tag) <= 16: + return False + + # 基于字符集判断(仅包含数字和字母) + if not re.match("^[0-9]*$", tag): + return False + + return True \ No newline at end of file diff --git a/sf_machine_connect/views/WorkCenterBarcodes.xml b/sf_machine_connect/views/WorkCenterBarcodes.xml index 9e5d3982..97fee70e 100644 --- a/sf_machine_connect/views/WorkCenterBarcodes.xml +++ b/sf_machine_connect/views/WorkCenterBarcodes.xml @@ -26,6 +26,7 @@ + diff --git a/sf_machine_connect/views/machine_monitor.xml b/sf_machine_connect/views/machine_monitor.xml index c6964659..5a05778f 100644 --- a/sf_machine_connect/views/machine_monitor.xml +++ b/sf_machine_connect/views/machine_monitor.xml @@ -18,6 +18,7 @@ + diff --git a/sf_machine_connect/views/maintenance_views.xml b/sf_machine_connect/views/maintenance_views.xml new file mode 100644 index 00000000..82c5aff4 --- /dev/null +++ b/sf_machine_connect/views/maintenance_views.xml @@ -0,0 +1,17 @@ + + + + + sf.machine.hr.equipment.view.tree.inherit + maintenance.equipment + + + +
+
+
+
+
+ +
\ No newline at end of file diff --git a/sf_maintenance/models/sf_maintenance_oee.py b/sf_maintenance/models/sf_maintenance_oee.py index 41301410..9a6aaba8 100644 --- a/sf_maintenance/models/sf_maintenance_oee.py +++ b/sf_maintenance/models/sf_maintenance_oee.py @@ -41,29 +41,32 @@ class SfMaintenanceEquipmentOEELog(models.Model): _name = 'maintenance.equipment.oee.logs' _description = '设备运行日志' - equipment_id = fields.Many2one('maintenance.equipment', '机台号') - equipment_code = fields.Char('设备编码') + equipment_id = fields.Many2one('maintenance.equipment', '机台号', readonly='True') + equipment_code = fields.Char('设备编码', readonly='True') name = fields.Char('设备名称', readonly='True') + function_type = fields.Selection( + [("ZXJGZX", "钻铣加工中心"), ("CXJGZX", "车削加工中心"), ("FHJGZX", "复合加工中心")], + default="", string="功能类型") machine_tool_picture = fields.Binary('设备图片') - type_id = fields.Many2one('sf.machine_tool.type', '品牌型号') + type_id = fields.Many2one('sf.machine_tool.type', '品牌型号', reaonly='True') state = fields.Selection([("加工", "加工"), ("关机", "关机"), ("待机", "待机"), ("故障", "故障"), ("检修", "检修"), ("保养", "保养")], default="", string="实时状态") - online_time = fields.Char('开机时长') + online_time = fields.Char('开机时长', reaonly='True') - offline_time = fields.Char('关机时长') - offline_nums = fields.Integer('关机次数') + offline_time = fields.Char('关机时长', reaonly='True') + offline_nums = fields.Integer('关机次数', reaonly='True') # 待机时长 - idle_time = fields.Char('待机时长') + idle_time = fields.Char('待机时长', reaonly='True') # 待机率 - idle_rate = fields.Char('待机率') + idle_rate = fields.Char('待机率', reaonly='True') - work_time = fields.Char('加工时长') - work_rate = fields.Char('可用率') - fault_time = fields.Char('故障时长') - fault_rate = fields.Char('故障率') - fault_nums = fields.Integer('故障次数') + work_time = fields.Char('加工时长', reaonly='True') + work_rate = fields.Char('可用率', reaonly='True') + fault_time = fields.Char('故障时长', reaonly='True') + fault_rate = fields.Char('故障率', reaonly='True') + fault_nums = fields.Integer('故障次数', reaonly='True') detail_ids = fields.One2many('maintenance.equipment.oee.log.detail', 'log_id', string='日志详情') @@ -81,12 +84,15 @@ class SfMaintenanceEquipmentOEELog(models.Model): class SfMaintenanceEquipmentOEELogDetail(models.Model): _name = 'maintenance.equipment.oee.log.detail' _description = '设备运行日志详情' + _order = 'time desc' - sequence = fields.Integer('序号') + # sequence = fields.Integer('序号', related='id') time = fields.Datetime('时间') state = fields.Selection([("加工", "加工"), ("关机", "关机"), ("待机", "待机"), ("故障", "故障"), ("检修", "检修"), ("保养", "保养")], default="", string="事件/状态") - production_id = fields.Many2one('mrp.production', '加工工单') + production_name = fields.Char('加工工单') log_id = fields.Many2one('maintenance.equipment.oee.logs', '日志') + # equipment_code = fields.Char('设备编码', related='log_id.equipment_code') + equipment_code = fields.Char('设备编码', readonly='True') diff --git a/sf_maintenance/views/maintenance_logs_views.xml b/sf_maintenance/views/maintenance_logs_views.xml index 0d172285..b3922595 100644 --- a/sf_maintenance/views/maintenance_logs_views.xml +++ b/sf_maintenance/views/maintenance_logs_views.xml @@ -159,6 +159,8 @@ + +
@@ -202,10 +204,10 @@ - + - + @@ -219,10 +221,10 @@ - + - + @@ -263,10 +265,10 @@ maintenance.equipment.oee.log.detail - + - + @@ -280,10 +282,10 @@ - + - + diff --git a/sf_manufacturing/__manifest__.py b/sf_manufacturing/__manifest__.py index 39da4482..35501367 100644 --- a/sf_manufacturing/__manifest__.py +++ b/sf_manufacturing/__manifest__.py @@ -15,12 +15,14 @@ 'data/stock_data.xml', 'data/empty_racks_data.xml', 'data/panel_data.xml', + 'data/agv_scheduling_data.xml', 'security/group_security.xml', 'security/ir.model.access.csv', 'wizard/workpiece_delivery_views.xml', 'wizard/rework_wizard_views.xml', 'wizard/production_wizard_views.xml', 'views/mrp_views_menus.xml', + 'views/agv_scheduling_views.xml', 'views/stock_lot_views.xml', 'views/mrp_production_addional_change.xml', 'views/mrp_routing_workcenter_view.xml', @@ -30,7 +32,7 @@ 'views/model_type_view.xml', 'views/agv_setting_views.xml', 'views/sf_maintenance_equipment.xml', - + 'views/res_config_settings_views.xml', ], 'assets': { @@ -40,7 +42,9 @@ 'web.assets_backend': [ 'sf_manufacturing/static/src/xml/kanban_change.xml', 'sf_manufacturing/static/src/js/kanban_change.js', - 'sf_manufacturing/static/src/scss/kanban_change.scss' + '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', ] }, diff --git a/sf_manufacturing/controllers/controllers.py b/sf_manufacturing/controllers/controllers.py index babda164..632e9e4c 100644 --- a/sf_manufacturing/controllers/controllers.py +++ b/sf_manufacturing/controllers/controllers.py @@ -2,6 +2,8 @@ import logging import json from datetime import datetime + +from odoo.addons.sf_manufacturing.models.agv_scheduling import RepeatTaskException from odoo import http from odoo.http import request @@ -145,7 +147,7 @@ class Manufacturing_Connect(http.Controller): logging.info('get_qcCheck error:%s' % e) return json.JSONEncoder().encode(res) - @http.route('/AutoDeviceApi/FeedBackStart', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, + @http.route('/AutoDeviceApi/FeedBackStart', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") def button_Work_START(self, **kw): """ @@ -193,7 +195,7 @@ class Manufacturing_Connect(http.Controller): logging.info('button_Work_START error:%s' % e) return json.JSONEncoder().encode(res) - @http.route('/AutoDeviceApi/FeedBackEnd', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, + @http.route('/AutoDeviceApi/FeedBackEnd', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") def button_Work_End(self, **kw): """ @@ -244,7 +246,7 @@ class Manufacturing_Connect(http.Controller): logging.info('button_Work_End error:%s' % e) return json.JSONEncoder().encode(res) - @http.route('/AutoDeviceApi/PartQualityInspect', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, + @http.route('/AutoDeviceApi/PartQualityInspect', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") def PartQualityInspect(self, **kw): """ @@ -290,7 +292,7 @@ class Manufacturing_Connect(http.Controller): logging.info('PartQualityInspect error:%s' % e) return json.JSONEncoder().encode(res) - @http.route('/AutoDeviceApi/CMMProgDolod', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, + @http.route('/AutoDeviceApi/CMMProgDolod', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") def CMMProgDolod(self, **kw): """ @@ -330,7 +332,7 @@ class Manufacturing_Connect(http.Controller): logging.info('CMMProgDolod error:%s' % e) return json.JSONEncoder().encode(res) - @http.route('/AutoDeviceApi/NCProgDolod', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, + @http.route('/AutoDeviceApi/NCProgDolod', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") def NCProgDolod(self, **kw): """ @@ -386,7 +388,7 @@ class Manufacturing_Connect(http.Controller): ret = json.loads(datas) request.env['center_control.interface.log'].sudo().create( {'content': ret, 'name': 'AutoDeviceApi/LocationChange'}) - logging.info('LocationChange_ret===========:%s' % ret) + logging.info('库位变更LocationChange_ret:%s' % ret) RfidCode = ret['RfidCode'] ChangeType = ret['ChangeType'] OldDeciveId = ret['OldDeciveId'] @@ -396,34 +398,80 @@ class Manufacturing_Connect(http.Controller): OldDeciveStart = ret['OldDeciveStart'] OldDeciveEnd = ret['OldDeciveEnd'] - temp_val_sn_id = None - old_localtion = None - # if ChangeType == 'Part' or ChangeType == 'Tool': - stock_lot_obj = request.env['stock.lot'].sudo().search( - [('rfid', '=', RfidCode)], limit=1) - logging.info('stock_lot_obj===========:%s' % stock_lot_obj) - if not stock_lot_obj: - res = {'Succeed': False, 'ErrorCode': 202, 'Error': '未根据RfidCode找到该产品'} - return json.JSONEncoder().encode(res) - if OldPosition: - old_localtion = request.env['sf.shelf.location'].sudo().search( - [('barcode', '=', OldPosition)], limit=1) - logging.info('old_localtion===========:%s' % old_localtion) - new_localtion = request.env['sf.shelf.location'].sudo().search( - [('barcode', '=', NewPosition)], limit=1) - logging.info('new_localtion===========:%s' % new_localtion) - if not new_localtion: - res = {'Succeed': False, 'ErrorCode': 202, 'Error': '没有该目标位置'} - return json.JSONEncoder().encode(res) - if old_localtion: - temp_val_sn_id = old_localtion.product_sn_id - logging.info('temp_val_sn_id===========:%s' % temp_val_sn_id) - old_localtion.product_sn_id = None - new_localtion.product_sn_id = temp_val_sn_id - logging.info('====1======') - else: - new_localtion.product_sn_id = stock_lot_obj.id - logging.info('=====2======') + if ChangeType == 'Part': + temp_val_sn_id = None + old_localtion = None + # if ChangeType == 'Part' or ChangeType == 'Tool': + stock_lot_obj = request.env['stock.lot'].sudo().search( + [('rfid', '=', RfidCode)], limit=1) + logging.info('stock_lot_obj===========:%s' % stock_lot_obj) + if not stock_lot_obj: + res = {'Succeed': False, 'ErrorCode': 202, 'Error': '未根据RfidCode找到该产品'} + return json.JSONEncoder().encode(res) + if OldPosition: + old_localtion = request.env['sf.shelf.location'].sudo().search( + [('barcode', '=', OldPosition)], limit=1) + logging.info('old_localtion===========:%s' % old_localtion) + new_localtion = request.env['sf.shelf.location'].sudo().search( + [('barcode', '=', NewPosition)], limit=1) + logging.info('new_localtion===========:%s' % new_localtion) + if not new_localtion: + res = {'Succeed': False, 'ErrorCode': 202, 'Error': '没有该目标位置'} + return json.JSONEncoder().encode(res) + if old_localtion: + temp_val_sn_id = old_localtion.product_sn_id + logging.info('temp_val_sn_id===========:%s' % temp_val_sn_id) + old_localtion.product_sn_id = None + new_localtion.product_sn_id = temp_val_sn_id + logging.info('====1======') + else: + new_localtion.product_sn_id = stock_lot_obj.id + logging.info('=====2======') + elif ChangeType == 'Tool': + # 对功能刀具库位变更信息进行更改 + def write_tool(DeciveId): + if 'Tool' in DeciveId: + shelfinfo = list(filter(lambda x: x.get('DeviceId') == DeciveId, + request.env['sf.shelf.location'].sudo().get_sf_shelf_location_info( + DeciveId))) + total_data = request.env['sf.shelf.location.datasync'].sudo().get_total_data() + for item in shelfinfo: + logging.info('货架已获取信息:%s' % item) + shelf_barcode = request.env['sf.shelf.location.datasync'].sudo().find_our_code( + total_data, item['Postion']) + location_id = request.env['sf.shelf.location'].sudo().search( + [('barcode', '=', shelf_barcode)], + limit=1) + if location_id: + # 如果是线边刀库信息,则对功能刀具移动生成记录 + if 'Tool' in item['Postion']: + tool = request.env['sf.functional.cutting.tool.entity'].sudo().search( + [('rfid', '=', item['RfidCode']), ('functional_tool_status', '!=', '已拆除')]) + tool.sudo().tool_in_out_stock_location(location_id) + if tool: + location_id.product_sn_id = tool.barcode_id.id + # 修改功能刀具状态 + if item.get('State') == '报警': + if tool.functional_tool_status != item.get('State'): + tool.write({ + 'functional_tool_status': item['State'] + }) + else: + location_id.product_sn_id = False + if item['RfidCode']: + logging.info('Rfid为【%s】的功能刀具在系统中不存在!' % item['RfidCode']) + else: + equipment_id = request.env['maintenance.equipment'].sudo().search([('name', '=', DeciveId)]) + if equipment_id: + equipment_id.sudo().register_equipment_tool() + else: + res_1 = {'Succeed': False, 'ErrorCode': 202, 'Error': f'设备【{DeciveId}】不存在'} + return json.JSONEncoder().encode(res_1) + + if OldDeciveId: + write_tool(OldDeciveId) + elif NewDeciveId: + write_tool(NewDeciveId) except Exception as e: res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} logging.info('LocationChange error:%s' % e) @@ -448,13 +496,16 @@ class Manufacturing_Connect(http.Controller): if 'DeviceId' in ret: logging.info('DeviceId:%s' % ret['DeviceId']) if 'IsComplete' in ret: + rfid_codes = [] + workorder_ids = [] if ret['IsComplete'] is True or ret['IsComplete'] is False: for i in range(1, 5): logging.info('F-RfidCode:%s' % i) if f'RfidCode{i}' in ret: rfid_code = ret[f'RfidCode{i}'] logging.info('RfidCode:%s' % rfid_code) - if rfid_code is not None: + if rfid_code is not None and rfid_code != '': + rfid_codes.append(rfid_code) domain = [ ('rfid_code', '=', rfid_code), ('routing_type', '=', 'CNC加工'), ('state', '!=', 'rework') @@ -462,6 +513,7 @@ class Manufacturing_Connect(http.Controller): workorder = request.env['mrp.workorder'].sudo().search(domain, order='id asc') if workorder: for order in workorder: + workorder_ids.append(order.id) if order.production_line_state == '待上产线': logging.info( '工单产线状态:%s' % order.production_line_state) @@ -470,23 +522,30 @@ class Manufacturing_Connect(http.Controller): ('processing_panel', '=', order.processing_panel)]) if panel_workorder: panel_workorder.write({'production_line_state': '已上产线'}) - 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.status == '待下发': - workpiece_delivery.write({'is_manual_work': True}) + # 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.status == '待下发': + # workpiece_delivery.write({'is_manual_work': True}) + # 下发 else: res = {'Succeed': False, 'ErrorCode': 204, 'Error': 'DeviceId为%s没有对应的已配送工件数据' % ret['DeviceId']} + if ret['IsComplete'] is True: + # 向AGV任务调度下发运送空料架任务 + workorders = request.env['mrp.workorder'].browse(workorder_ids) + request.env['sf.agv.scheduling'].add_scheduling(ret['DeviceId'], '运送空料架', workorders) else: res = {'Succeed': False, 'ErrorCode': 203, 'Error': '未传IsComplete字段'} else: res = {'Succeed': False, 'ErrorCode': 201, 'Error': '未传DeviceId字段'} + except RepeatTaskException as e: + logging.info('AGVToProduct error:%s' % e) except Exception as e: - res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} + res = {'Succeed': False, 'ErrorCode': 202, 'Error': str(e)} logging.info('AGVToProduct error:%s' % e) return json.JSONEncoder().encode(res) @@ -509,7 +568,8 @@ class Manufacturing_Connect(http.Controller): logging.info('ret:%s' % ret) if 'DeviceId' in ret: logging.info('DeviceId:%s' % ret['DeviceId']) - delivery_Arr = [] + # delivery_Arr = [] + workorder_ids = [] if 'IsComplete' in ret: if ret['IsComplete'] is True or ret['IsComplete'] is False: for i in range(1, 5): @@ -517,7 +577,7 @@ class Manufacturing_Connect(http.Controller): if f'RfidCode{i}' in ret: rfid_code = ret[f'RfidCode{i}'] logging.info('RfidCode:%s' % rfid_code) - if rfid_code is not None: + if rfid_code is not None and rfid_code != '': domain = [ ('rfid_code', '=', rfid_code), ('routing_type', '=', 'CNC加工'), ('state', '!=', 'rework') @@ -525,6 +585,7 @@ class Manufacturing_Connect(http.Controller): workorder = request.env['mrp.workorder'].sudo().search(domain, order='id asc') if workorder: for order in workorder: + workorder_ids.append(order.id) if order.production_line_state == '已上产线': logging.info( '工单产线状态:%s' % order.production_line_state) @@ -534,35 +595,41 @@ 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) + # 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']} - if delivery_Arr: - logging.info('delivery_Arr:%s' % delivery_Arr) - delivery_workpiece = request.env['sf.workpiece.delivery'].sudo().search( - [('id', 'in', delivery_Arr)]) - if delivery_workpiece: - logging.info('开始向agv下发下产线任务') - agv_site = request.env['sf.agv.site'].sudo().search([]) - if agv_site: - has_site = agv_site.update_site_state() - if has_site is True: - is_free = delivery_workpiece._check_avgsite_state() - if is_free is True: - delivery_workpiece._delivery_avg() - logging.info('agv下发下产线任务下发完成') + # if delivery_Arr: + # logging.info('delivery_Arr:%s' % delivery_Arr) + # delivery_workpiece = request.env['sf.workpiece.delivery'].sudo().search( + # [('id', 'in', delivery_Arr)]) + # if delivery_workpiece: + # logging.info('开始向agv下发下产线任务') + # agv_site = request.env['sf.agv.site'].sudo().search([]) + # if agv_site: + # has_site = agv_site.update_site_state() + # if has_site is True: + # is_free = delivery_workpiece._check_avgsite_state() + # if is_free is True: + # delivery_workpiece._delivery_avg() + # logging.info('agv下发下产线任务下发完成') + if ret['IsComplete'] is True: + # 向AGV任务调度下发下产线任务 + workorders = request.env['mrp.workorder'].browse(workorder_ids) + request.env['sf.agv.scheduling'].add_scheduling(ret['DeviceId'], '下产线', workorders) else: res = {'Succeed': False, 'ErrorCode': 203, 'Error': '未传IsComplete字段'} + except RepeatTaskException as e: + logging.info('AGVToProduct error:%s' % e) except Exception as e: - res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} + res = {'Succeed': False, 'ErrorCode': 202, 'Error': str(e)} logging.info('AGVDownProduct error:%s' % e) return json.JSONEncoder().encode(res) @@ -600,3 +667,32 @@ class Manufacturing_Connect(http.Controller): res = {'Succeed': False, 'ErrorCode': 202, 'Error': e} logging.info('AGVDownProduct error:%s' % e) return json.JSONEncoder().encode(res) + + @http.route('/AutoDeviceApi/AgvStationState', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, + cors="*") + def AGVStationState(self, **kw): + """ + 中控推送接驳站状态 + :param kw: + :return: + """ + logging.info('AGVStationState:%s' % kw) + try: + res = {'Succeed': True} + datas = request.httprequest.data + ret = json.loads(datas) + request.env['center_control.interface.log'].sudo().create( + {'content': ret, 'name': 'AutoDeviceApi/AGVStationState'}) + logging.info('ret:%s' % ret) + ret = ret['param'] + params = {} + for i in range(len(ret)): + if 'DeviceId' in ret[i] and 'AtHome' in ret[i]: + logging.info('DeviceId:%s, AtHome:%s' % (ret[i]['DeviceId'], ret[i]['AtHome'])) + params[ret[i]['DeviceId']] = '占用' if ret[i]['AtHome'] else '空闲' + if params: + request.env['sf.agv.site'].update_site_state(params) + 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 diff --git a/sf_manufacturing/controllers/workpiece.py b/sf_manufacturing/controllers/workpiece.py index 5c5d6f22..370c71a0 100644 --- a/sf_manufacturing/controllers/workpiece.py +++ b/sf_manufacturing/controllers/workpiece.py @@ -8,7 +8,7 @@ from odoo.http import request class Workpiece(http.Controller): - @http.route('/agvApi/backfeed', type='json', auth='none', methods=['GET', 'POST'], csrf=False, + @http.route('/agvApi/backfeed', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, cors="*") def backfeed(self, **kw): """ @@ -25,15 +25,14 @@ class Workpiece(http.Controller): if 'reqCode' in ret: if 'method' in ret: if ret['method'] == 'end': - req_codes = ret['reqCode'].split(',') - for req_code in req_codes: - workpiece_delivery = request.env['sf.workpiece.delivery'].sudo().search( - [('name', '=', req_code.strip()), ('task_completion_time', '=', False)]) - if workpiece_delivery: - workpiece_delivery.write({'status': '已配送', 'task_completion_time': datetime.now()}) - else: - res = {'Succeed': False, 'ErrorCode': 203, - 'Error': '该reqCode暂未查到对应的工件配送记录'} + # 找到对应的AGV调度任务 + agv_scheduling = request.env['sf.agv.scheduling'].sudo().search( + [('name', '=', ret['reqCode']), ('state', '=', '配送中')]) + if agv_scheduling: + agv_scheduling.finish_scheduling() + else: + res = {'Succeed': False, 'ErrorCode': 203, + 'Error': '该reqCode暂未查到对应的AGV任务记录'} else: res = {'Succeed': False, 'ErrorCode': 204, 'Error': '未传method字段'} else: diff --git a/sf_manufacturing/data/agv_scheduling_data.xml b/sf_manufacturing/data/agv_scheduling_data.xml new file mode 100644 index 00000000..44878ade --- /dev/null +++ b/sf_manufacturing/data/agv_scheduling_data.xml @@ -0,0 +1,16 @@ + + + + + AGV调度 + sf.agv.scheduling + B%(year)s%(month)s%(day)s + 4 + 1 + standard + True + day + + + + \ No newline at end of file diff --git a/sf_manufacturing/models/__init__.py b/sf_manufacturing/models/__init__.py index 3de23ef3..7d6aa8ae 100644 --- a/sf_manufacturing/models/__init__.py +++ b/sf_manufacturing/models/__init__.py @@ -9,3 +9,5 @@ from . import stock from . import res_user from . import production_line_base from . import agv_setting +from . import agv_scheduling +from . import res_config_setting diff --git a/sf_manufacturing/models/agv_scheduling.py b/sf_manufacturing/models/agv_scheduling.py new file mode 100644 index 00000000..d232c643 --- /dev/null +++ b/sf_manufacturing/models/agv_scheduling.py @@ -0,0 +1,267 @@ +import requests + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +import logging + +_logger = logging.getLogger(__name__) + + +class RepeatTaskException(UserError): + pass + + +class AgvScheduling(models.Model): + _name = 'sf.agv.scheduling' + _description = 'agv调度' + _order = 'id desc' + + name = fields.Char('任务单号', index=True, copy=False) + + def _get_agv_route_type_selection(self): + return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection'] + + agv_route_type = fields.Selection(selection=_get_agv_route_type_selection, string='任务类型', required=True) + agv_route_id = fields.Many2one('sf.agv.task.route', '任务路线') + start_site_id = fields.Many2one('sf.agv.site', '起点接驳站', required=True) + end_site_id = fields.Many2one('sf.agv.site', '终点接驳站', tracking=True) + site_state = fields.Selection([ + ('占用', '占用'), + ('空闲', '空闲')], string='终点接驳站状态', default='占用') + state = fields.Selection([ + ('待下发', '待下发'), + ('配送中', '配送中'), + ('已配送', '已配送'), + ('已取消', '已取消')], string='状态', default='待下发', tracking=True) + workorder_ids = fields.Many2many('mrp.workorder', 'sf_agv_scheduling_mrp_workorder_ref', string='关联工单') + task_create_time = fields.Datetime('任务创建时间') + task_delivery_time = fields.Datetime('任务下发时间') + task_completion_time = fields.Datetime('任务完成时间') + task_duration = fields.Char('任务时长', compute='_compute_task_duration') + + @api.depends('agv_route_type') + def _compute_delivery_workpieces(self): + for record in self: + if record.agv_route_type == '运送空料架': + record.delivery_workpieces = '/' + else: + record.delivery_workpieces = '、'.join(record.workorder_ids.mapped('production_id.name')) + + delivery_workpieces = fields.Char('配送工件', compute=_compute_delivery_workpieces) + + @api.model + def web_search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, count_limit=None): + domain = domain or [] + new_domain = [] + for index, item in enumerate(domain): + if isinstance(item, list): + if item[0] == 'delivery_workpieces': + new_domain.append('&') + new_domain.append(['workorder_ids.production_id.name', item[1], item[2]]) + new_domain.append(['agv_route_type', '!=', '运送空料架']) + continue + new_domain.append(item) + + return super(AgvScheduling, self).web_search_read(new_domain, fields, limit=limit, offset=offset) + + @api.depends('task_completion_time', 'task_delivery_time') + def _compute_task_duration(self): + for rec in self: + if rec.task_completion_time and rec.task_delivery_time: + rec.task_duration = str(rec.task_completion_time - rec.task_delivery_time) + else: + rec.task_duration = '' + + @api.model_create_multi + def create(self, vals_list): + # We generate a standard reference + for vals in vals_list: + vals['name'] = self.env['ir.sequence'].next_by_code('sf.agv.scheduling') or _('New') + return super().create(vals_list) + + def add_scheduling(self, agv_start_site_name, agv_route_type, workorders): + """ add_scheduling(agv_start_site_id, agv_route_type, workorders) -> agv_scheduling + 新增AGV调度 + params: + agv_start_site_name: AGV起点接驳站名称 + agv_route_type: AGV任务类型 + workorders: 工单 + """ + _logger.info('创建AGV调度任务\r\n起点为【%s】,任务类型为【%s】,工单为【%s】' % (agv_start_site_name, agv_route_type, workorders)) + if not workorders: + raise UserError(_('工单不能为空')) + agv_start_site = self.env['sf.agv.site'].sudo().search([('name', '=', agv_start_site_name)], limit=1) + if not agv_start_site: + raise UserError(_('不存在名称为【%s】的接驳站,请先创建!' % agv_start_site_name)) + # 如果存在相同任务类型工单的AGV调度任务,则提示错误 + agv_scheduling = self.sudo().search([ + ('workorder_ids', 'in', workorders.ids), + ('agv_route_type', '=', agv_route_type), + ('state', 'in', ['待下发', '配送中']) + ], limit=1) + if agv_scheduling: + # 计算agv_scheduling.workorder_ids与workorders的交集 + repetitive_workorders = agv_scheduling.workorder_ids & workorders + raise RepeatTaskException( + '制造订单号【%s】已存在于【%s】AGV调度任务,请勿重复下发!' % + (','.join(repetitive_workorders.mapped('production_id.name')), agv_scheduling.name) + ) + + vals = { + 'start_site_id': agv_start_site.id, + 'agv_route_type': agv_route_type, + 'workorder_ids': workorders.ids, + # 'workpiece_delivery_ids': deliveries.mapped('id') if deliveries else [], + 'task_create_time': fields.Datetime.now() + } + # 如果只有唯一任务路线,则自动赋予终点接驳站跟任务名称 + agv_routes = self.env['sf.agv.task.route'].sudo().search([ + ('route_type', '=', agv_route_type), + ('start_site_id', '=', agv_start_site.id) + ]) + if not agv_routes: + raise UserError(_('不存在起点为【%s】的【%s】任务路线,请先创建!' % (agv_start_site_name, agv_route_type))) + idle_route = None + if len(agv_routes) == 1: + idle_route = agv_routes[0] + vals.update({'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id}) + else: + # 判断终点接驳站是否为空闲 + idle_routes = agv_routes.filtered(lambda r: r.end_site_id.state == '空闲') + if idle_routes: + # 将空闲的路线按照终点接驳站名称排序 + idle_routes = sorted(idle_routes, key=lambda r: r.end_site_id.name) + idle_route = idle_routes[0] + vals.update({'end_site_id': idle_route.end_site_id.id, 'agv_route_id': idle_route.id}) + try: + scheduling = self.env['sf.agv.scheduling'].sudo().create(vals) + # 触发空闲接驳站状态更新,触发新任务下发 + if idle_route and idle_route.end_site_id.state == '空闲': + scheduling.dispatch_scheduling(idle_route) + + except Exception as e: + _logger.error('添加AGV调度任务失败: %s', e) + raise UserError(_('添加AGV调度任务失败: %s', e)) + + return scheduling + + def on_site_state_change(self, agv_site_id, agv_site_state): + """ + 响应AGV接驳站站点状态变化 + params: + agv_site_id: 接驳站ID + agv_site_state: 站点状态('空闲', '占用') + """ + if agv_site_state == '空闲': + # 查询终点接驳站为agv_site_id的AGV路线 + task_routes = self.env['sf.agv.task.route'].sudo().search([('end_site_id', '=', agv_site_id)]) + agv_scheduling = self.env['sf.agv.scheduling'].sudo().search( + [('state', '=', '待下发'), ('agv_route_type', 'in', task_routes.mapped('route_type'))], + order='id asc', + limit=1 + ) + task_route = task_routes.filtered( + lambda r: r.start_site_id == agv_scheduling.start_site_id and r.start_site_id == agv_scheduling.start_site_id + ) + if task_route: + # 下发AGV调度任务并修改接驳站状态为占用 + agv_scheduling.dispatch_scheduling(task_route) + + def _delivery_avg(self): + config = self.env['res.config.settings'].get_values() + position_code_arr = [{ + 'positionCode': self.start_site_id.name, + 'code': '00' + }, { + 'positionCode': self.end_site_id.name, + 'code': '00' + }] + res = {'reqCode': self.name, 'reqTime': '', 'clientCode': '', 'tokenCode': '', + 'taskTyp': 'F01', 'ctnrTyp': '', 'ctnrCode': '', 'wbCode': config['wbcode'], + 'positionCodePath': position_code_arr, + 'podCode': '', + 'podDir': '', 'materialLot': '', 'priority': '', 'taskCode': '', 'agvCode': '', 'materialLot': '', + 'data': ''} + try: + logging.info('AGV请求路径:%s' % config['agv_rcs_url']) + logging.info('AGV-json:%s' % res) + headers = {'Content-Type': 'application/json'} + ret = requests.post((config['agv_rcs_url']), json=res, headers=headers) + ret = ret.json() + logging.info('config-ret:%s' % ret) + if ret['code'] == 0: + return True + else: + raise UserError(ret['message']) + except Exception as e: + logging.info('config-e:%s' % e) + raise UserError("工件配送请求agv失败:%s" % e) + + def button_cancel(self): + # 弹出二次确认窗口后执行 + for rec in self: + if rec.state != '待下发': + raise UserError('只有待下发状态的AGV调度任务才能取消!') + rec.state = '已取消' + + def finish_scheduling(self): + """ + 完成调度任务 + """ + for rec in self: + if rec.state != '配送中': + return False + _logger.info('AGV任务调度:完成任务%s' % rec) + rec.state = '已配送' + rec.task_completion_time = fields.Datetime.now() + + def dispatch_scheduling(self, agv_task_route): + """ + 下发调度任务 + params: + agv_route sf.agv.task.route对象 + """ + for rec in self: + if rec.state != '待下发': + return False + _logger.info('AGV任务调度:下发调度任务,路线为%s' % agv_task_route) + rec.state = '配送中' + rec.task_delivery_time = fields.Datetime.now() + rec.site_state = '空闲' + rec.end_site_id = agv_task_route.end_site_id.id + rec.agv_route_id = agv_task_route.id + is_agv_task_dispatch = self.env['ir.config_parameter'].sudo().get_param('is_agv_task_dispatch') + if is_agv_task_dispatch: + rec._delivery_avg() + # 更新接驳站状态 + rec.env['sf.agv.site'].update_site_state({rec.end_site_id.name: '占用'}, False) + + def write(self, vals): + if vals.get('state', False): + if vals['state'] == '已取消': + self.env['sf.workpiece.delivery'].search([('agv_scheduling_id', '=', self.id)]).write({'status': '待下发'}) + elif vals['state'] == '已配送': + self.env['sf.workpiece.delivery'].search([('agv_scheduling_id', '=', self.id)]).write({ + 'status': '已配送', + 'feeder_station_destination_id': self.end_site_id.id, + 'route_id': self.agv_route_id.id, + 'task_completion_time': fields.Datetime.now() + }) + elif vals['state'] == '配送中': + self.env['sf.workpiece.delivery'].search([('agv_scheduling_id', '=', self.id)]).write({ + 'feeder_station_destination_id': self.end_site_id.id, + 'route_id': self.agv_route_id.id, + 'task_delivery_time': fields.Datetime.now() + }) + return super().write(vals) + + +class ResMrpWorkOrder(models.Model): + _inherit = 'mrp.workorder' + + agv_scheduling_ids = fields.Many2many( + 'sf.agv.scheduling', + 'sf_agv_scheduling_mrp_workorder_ref', + string='AGV调度', + domain=[('state', '!=', '已取消')]) diff --git a/sf_manufacturing/models/agv_setting.py b/sf_manufacturing/models/agv_setting.py index 06f9edba..820ed539 100644 --- a/sf_manufacturing/models/agv_setting.py +++ b/sf_manufacturing/models/agv_setting.py @@ -5,50 +5,77 @@ import time from odoo import fields, models, api from odoo.exceptions import UserError +_logger = logging.getLogger(__name__) + class AgvSetting(models.Model): _name = 'sf.agv.site' _description = 'agv站点' name = fields.Char('位置编号') - owning_region = fields.Char('所属区域') + # owning_region = fields.Char('所属区域') state = fields.Selection([ ('占用', '占用'), ('空闲', '空闲')], string='状态') divide_the_work = fields.Char('主要分工') active = fields.Boolean('有效', default=True) + workcenter_id = fields.Many2one(string='所属区域', comodel_name='mrp.workcenter', tracking=True, + domain=[('is_agv_scheduling', '=', True)]) - def update_site_state(self): - # 调取中控的接驳站接口并修改对应站点的状态 - config = self.env['res.config.settings'].get_values() - # token = sf_sync_config['token'Ba F2CF5DCC-1A00-4234-9E95-65603F70CC8A] - headers = {'Authorization': config['center_control_Authorization']} - center_control_url = config['center_control_url'] + "/AutoDeviceApi/GetAgvStationState?date=" - timestamp = int(time.time()) - center_control_url += str(timestamp) - logging.info('工件配送-请求中控地址:%s' % center_control_url) - try: - center_control_r = requests.get(center_control_url, headers=headers, timeout=10) # 设置超时为60秒 - ret = center_control_r.json() - logging.info('工件配送-请求中控站点信息:%s' % ret) - self.env['center_control.interface.log'].sudo().create( - {'content': ret, 'name': 'AutoDeviceApi/GetAgvStationState?date=%s' % str(timestamp)}) - if ret['Succeed'] is True: - datas = ret['Datas'] - for item in self: - for da in datas: - if da['DeviceId'] == item.name: - if da['AtHome'] is True: - item.state = '占用' - else: - item.state = '空闲' - return True - except requests.exceptions.Timeout: - logging.error('工件配送-请求中控接口超时') - return False - except requests.exceptions.RequestException as e: - logging.error('工件配送-请求中控接口错误: %s', e) - return False + # name必须唯一 + _sql_constraints = [ + ('name_uniq', 'unique (name)', '站点编号必须唯一!'), + ] + + # def update_site_state(self): + # # 调取中控的接驳站接口并修改对应站点的状态 + # config = self.env['res.config.settings'].get_values() + # # token = sf_sync_config['token'Ba F2CF5DCC-1A00-4234-9E95-65603F70CC8A] + # headers = {'Authorization': config['center_control_Authorization']} + # center_control_url = config['center_control_url'] + "/AutoDeviceApi/GetAgvStationState?date=" + # timestamp = int(time.time()) + # center_control_url += str(timestamp) + # logging.info('工件配送-请求中控地址:%s' % center_control_url) + # try: + # center_control_r = requests.get(center_control_url, headers=headers, timeout=10) # 设置超时为60秒 + # ret = center_control_r.json() + # logging.info('工件配送-请求中控站点信息:%s' % ret) + # self.env['center_control.interface.log'].sudo().create( + # {'content': ret, 'name': 'AutoDeviceApi/GetAgvStationState?date=%s' % str(timestamp)}) + # if ret['Succeed'] is True: + # datas = ret['Datas'] + # for item in self: + # for da in datas: + # if da['DeviceId'] == item.name: + # if da['AtHome'] is True: + # item.state = '占用' + # else: + # item.state = '空闲' + # return True + # except requests.exceptions.Timeout: + # logging.error('工件配送-请求中控接口超时') + # return False + # except requests.exceptions.RequestException as e: + # logging.error('工件配送-请求中控接口错误: %s', e) + # return False + + def update_site_state(self, agv_site_state_arr, notify=True): + """ + 更新接驳站状态 + params: + agv_site_state_arr: {'A01': '空闲', 'B01': '占用'} + notify: 是否通知调度(非中控发起的状态改变不触发调度任务) + """ + if isinstance(agv_site_state_arr, dict): + for agv_site_name, is_occupy in agv_site_state_arr.items(): + agv_site = self.env['sf.agv.site'].sudo().search([('name', '=', agv_site_name)]) + if agv_site: + agv_site.state = is_occupy + if notify: + self.env['sf.agv.scheduling'].on_site_state_change(agv_site.id, agv_site.state) + else: + _logger.error("更新失败:接驳站站点错误!%s" % agv_site_name) + raise UserError("更新失败:接驳站站点错误!") class AgvTaskRoute(models.Model): @@ -71,6 +98,17 @@ class AgvTaskRoute(models.Model): if self.end_site_id == self.start_site_id: raise UserError("您选择的终点接驳站与起点接驳站重复,请重新选择") + workcenter_id = fields.Many2one(string='所属区域', comodel_name='mrp.workcenter', domain=[('is_agv_scheduling', '=', True)], + compute="_compute_region") + + @api.depends('end_site_id') + def _compute_region(self): + for record in self: + if record.end_site_id: + record.workcenter_id = record.end_site_id.workcenter_id + else: + record.workcenter_id = None + class Center_controlInterfaceLog(models.Model): _name = 'center_control.interface.log' diff --git a/sf_manufacturing/models/mrp_production.py b/sf_manufacturing/models/mrp_production.py index 8dc02132..5095556d 100644 --- a/sf_manufacturing/models/mrp_production.py +++ b/sf_manufacturing/models/mrp_production.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- import base64 +import datetime import logging import json import os import re import requests from itertools import groupby -from odoo import api, fields, models, _ +from collections import defaultdict, namedtuple +from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.exceptions import UserError, ValidationError from odoo.addons.sf_base.commons.common import Common from odoo.tools import float_compare, float_round, float_is_zero, format_datetime @@ -77,9 +79,10 @@ class MrpProduction(models.Model): ('pending_cam', '待加工'), ('progress', '加工中'), ('rework', '返工'), + ('scrap', '报废'), ('to_close', 'To Close'), ('done', 'Done'), - ('cancel', '报废')], string='State', + ('cancel', '已取消')], string='State', compute='_compute_state', copy=False, index=True, readonly=True, store=True, tracking=True, help=" * Draft: The MO is not confirmed yet.\n" @@ -120,8 +123,39 @@ class MrpProduction(models.Model): # 上传零件图纸 part_drawing = fields.Binary('零件图纸') - manual_quotation = fields.Boolean('人工编程', default=False, readonly=True) + @api.depends('product_id.manual_quotation') + def _compute_manual_quotation(self): + for item in self: + item.manual_quotation = item.product_id.manual_quotation + + manual_quotation = fields.Boolean('人工编程', default=False, compute=_compute_manual_quotation, store=True) is_scrap = fields.Boolean('是否报废', default=False) + is_remanufacture = fields.Boolean('是否重新制造', default=False) + remanufacture_count = fields.Integer("重新制造订单数量", compute='_compute_remanufacture_production_ids') + remanufacture_production_id = fields.Many2one('mrp.production', string='') + + @api.depends('remanufacture_production_id') + def _compute_remanufacture_production_ids(self): + for production in self: + if production.remanufacture_production_id: + remanufacture_production = self.env['mrp.production'].search( + [('id', '=', production.remanufacture_production_id.id)]) + if remanufacture_production: + production.remanufacture_count = len(remanufacture_production) + else: + production.remanufacture_count = 0 + + def action_view_remanufacture_productions(self): + self.ensure_one() + mrp_production = self.env['mrp.production'].search( + [('id', '=', self.remanufacture_production_id.id)]) + action = { + 'res_model': 'mrp.production', + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_id': mrp_production.id, + } + return action @api.depends( 'move_raw_ids.state', 'move_raw_ids.quantity_done', 'move_finished_ids.state', 'tool_state', @@ -166,7 +200,7 @@ class MrpProduction(models.Model): production.state = 'pending_cam' if production.state == 'progress': - if all(wo_state not in ('progress', 'done', 'rework') for wo_state in + if all(wo_state not in ('progress', 'done', 'rework', 'scrap') for wo_state in production.workorder_ids.mapped('state')): production.state = 'pending_cam' if production.is_rework is True: @@ -184,6 +218,11 @@ class MrpProduction(models.Model): for wo in production.workorder_ids): production.state = 'rework' + if any(wo.test_results == '报废' and wo.state == 'done' for wo in production.workorder_ids): + production.state = 'scrap' + if any(dr.test_results == '报废' and dr.handle_result == '已处理' for dr in + production.detection_result_ids): + production.state = 'cancel' # 如果制造订单的功能刀具为【无效刀】则制造订单状态改为返工 if production.tool_state == '2': production.state = 'rework' @@ -437,44 +476,45 @@ class MrpProduction(models.Model): # self.env['mrp.workorder'].json_workorder_str(k, production, route)) # 表面工艺工序 # 获取表面工艺id - if production.product_id.model_process_parameters_ids: - logging.info('model_process_parameters_ids:%s' % production.product_id.model_process_parameters_ids) - surface_technics_arr = [] - # 工序id - route_workcenter_arr = [] - for item in production.product_id.product_model_type_id.surface_technics_routing_tmpl_ids: - surface_technics_arr.append(item.route_workcenter_id.surface_technics_id.id) - route_workcenter_arr.append(item.route_workcenter_id.id) - if surface_technics_arr: - production_process_category = self.env['sf.production.process.category'].search( - [('production_process_ids.id', 'in', surface_technics_arr)], - order='sequence asc' - ) - # 用filter刷选表面工艺id'是否存在工艺类别对象里 - if production_process_category: - for p in production_process_category: - logging.info('production_process_category:%s' % p.name) - production_process = p.production_process_ids.filtered( - lambda pp: pp.id in surface_technics_arr) - if production_process: - process_parameter = production.product_id.model_process_parameters_ids.filtered( - lambda pm: pm.process_id.id == production_process.id) - if process_parameter: - # 产品为表面工艺服务的供应商 - product_production_process = self.env['product.template'].search( - [('server_product_process_parameters_id', '=', process_parameter.id)]) - if product_production_process: - route_production_process = self.env[ - 'mrp.routing.workcenter'].search( - [('surface_technics_id', '=', production_process.id), - ('id', 'in', route_workcenter_arr)]) - if route_production_process: - workorders_values.append( - self.env[ - 'mrp.workorder']._json_workorder_surface_process_str( - production, route_production_process, - process_parameter, - product_production_process.seller_ids[0].partner_id.id)) + # 工序id + surface_technics_arr = [] + route_workcenter_arr = [] + for item in production.product_id.product_model_type_id.surface_technics_routing_tmpl_ids: + if item.route_workcenter_id.surface_technics_id.id: + for process_param in production.product_id.model_process_parameters_ids: + logging.info('process_param:%s%s' % (process_param.id, process_param.name)) + if item.route_workcenter_id.surface_technics_id == process_param.process_id: + logging.info( + 'surface_technics_id:%s%s' % (item.route_workcenter_id.surface_technics_id.id, + item.route_workcenter_id.surface_technics_id.name)) + surface_technics_arr.append(item.route_workcenter_id.surface_technics_id.id) + route_workcenter_arr.append(item.route_workcenter_id.id) + if surface_technics_arr: + production_process = self.env['sf.production.process'].search( + [('id', 'in', surface_technics_arr)], + order='sequence asc' + ) + for p in production_process: + logging.info('production_process:%s' % p.name) + # if production_process: + process_parameter = production.product_id.model_process_parameters_ids.filtered( + lambda pm: pm.process_id.id == p.id) + if process_parameter: + # 产品为表面工艺服务的供应商 + product_production_process = self.env['product.template'].search( + [('server_product_process_parameters_id', '=', process_parameter.id)]) + if product_production_process: + route_production_process = self.env[ + 'mrp.routing.workcenter'].search( + [('surface_technics_id', '=', p.id), + ('id', 'in', route_workcenter_arr)]) + if route_production_process: + workorders_values.append( + self.env[ + 'mrp.workorder']._json_workorder_surface_process_str( + production, route_production_process, + process_parameter, + product_production_process.seller_ids[0].partner_id.id)) elif production.product_id.categ_id.type == '坯料': embryo_routing_workcenter = self.env['sf.embryo.model.type.routing.sort'].search( [('embryo_model_type_id', '=', production.product_id.embryo_model_type_id.id)], @@ -624,7 +664,7 @@ class MrpProduction(models.Model): # 表面工艺工序 # 模型类型的表面工艺工序模版 surface_tmpl_ids = model_type_id.surface_technics_routing_tmpl_ids - # 产品选择的表面工艺 + # 产品选择的表面工艺参数 model_process_parameters_ids = rec.product_id.model_process_parameters_ids process_dict = {} if model_process_parameters_ids: @@ -633,7 +673,7 @@ class MrpProduction(models.Model): for surface_tmpl_id in surface_tmpl_ids: if process_id == surface_tmpl_id.route_workcenter_id.surface_technics_id: surface_tmpl_name = surface_tmpl_id.route_workcenter_id.name - process_dict.update({int(process_id.category_id.code): '%s-%s' % ( + process_dict.update({int(process_id.sequence): '%s-%s' % ( surface_tmpl_name, process_parameters_id.name)}) process_list = sorted(process_dict.keys()) for process_num in process_list: @@ -650,14 +690,16 @@ class MrpProduction(models.Model): raise ValidationError('该产品【加工面板】为空!') else: raise ValidationError('该产品没有选择【模版类型】!') - + logging.info('sequence_list: %s' % sequence_list) for work in rec.workorder_ids: - if sequence_list.get(work.name): - work.sequence = sequence_list[work.name] + work_name = work.name + logging.info(work_name) + if sequence_list.get(work_name): + work.sequence = sequence_list[work_name] elif sequence_list.get(work.processing_panel): processing_panel = sequence_list.get(work.processing_panel) - if processing_panel.get(work.name): - work.sequence = processing_panel[work.name] + if processing_panel.get(work_name): + work.sequence = processing_panel[work_name] else: raise ValidationError('工序【%s】在产品选择的模版类型中不存在!' % work.name) else: @@ -683,8 +725,9 @@ class MrpProduction(models.Model): sequence_max += 1 panel_sequence_list.update({tmpl_id.route_workcenter_id.name: sequence_max}) for work_id in work_ids: - if panel_sequence_list.get(work_id.name): - work_id.sequence = panel_sequence_list[work_id.name] + work_name = work_id.name + if panel_sequence_list.get(work_name): + work_id.sequence = panel_sequence_list[work_name] # 创建工单并进行排序 def _create_workorder(self, item): @@ -692,6 +735,52 @@ class MrpProduction(models.Model): self._reset_work_order_sequence() return True + def process_range_time(self): + for production in self: + works = production.workorder_ids + pro_plan = self.env['sf.production.plan'].search([('production_id', '=', production.id)], limit=1) + if not pro_plan: + continue + type_map = {'装夹预调': False, 'CNC加工': False, '解除装夹': False} + # 最后一次加工结束时间 + last_time = pro_plan.date_planned_start + # 预置时间 + for work in works: + count = type_map.get(work.routing_type) + date_planned_end = None + date_planned_start = None + duration_expected = datetime.timedelta(minutes=work.duration_expected) + reserve_time = datetime.timedelta(minutes=work.reserved_duration) + if not count: + # 第一轮加工 + if work.routing_type == '装夹预调': + date_planned_end = last_time - reserve_time + date_planned_start = date_planned_end - duration_expected + elif work.routing_type == 'CNC加工': + date_planned_start = last_time + date_planned_end = last_time + duration_expected + last_time = date_planned_end + else: + date_planned_start = last_time + reserve_time + date_planned_end = date_planned_start + duration_expected + last_time = date_planned_end + type_map.update({work.routing_type: True}) + else: + date_planned_start = last_time + reserve_time + date_planned_end = date_planned_start + duration_expected + last_time = date_planned_end + work.leave_id.write({ + 'date_from': date_planned_start, + 'date_to': date_planned_end, + }) + # work.write({'date_planned_start': date_planned_start, 'date_planned_finished': date_planned_end}) + work.date_planned_start = date_planned_start + work.date_planned_finished = date_planned_end + routing_workcenter = self.env['mrp.routing.workcenter'].sudo().search( + [('name', '=', work.routing_type)]) + + work.write({'date_planned_start': date_planned_start, 'date_planned_finished': date_planned_end,'duration_expected':routing_workcenter.time_cycle}) + # 修改标记已完成方法 def button_mark_done1(self): if not self.workorder_ids.filtered(lambda w: w.routing_type not in ['表面工艺']): @@ -788,6 +877,23 @@ class MrpProduction(models.Model): }) return action + # 报废 + def button_scrap_new(self): + cloud_programming = self._cron_get_programming_state() + return { + 'name': _('报废'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'sf.production.wizard', + 'target': 'new', + 'context': { + 'default_production_id': self.id, + 'default_reprogramming_num': cloud_programming['reprogramming_num'], + 'default_programming_states': cloud_programming['programming_state'], + 'default_is_reprogramming': True if cloud_programming['programming_state'] in ['已下发'] else False + } + } + # 返工 def button_rework(self): cloud_programming = None @@ -905,7 +1011,6 @@ class MrpProduction(models.Model): # production.write( # {'state': 'progress', 'programming_state': '已编程', 'is_rework': False}) # logging.info('返工含有已编程未下发的程序更新完成:%s' % production.name) - logging.info('更新程序完成:%s' % production.name) else: raise UserError(result['message']) @@ -913,116 +1018,114 @@ class MrpProduction(models.Model): logging.info('get_new_program error:%s' % e) raise UserError("从云平台获取最新程序失败,请联系管理员") - def recreateManufacturing(self): + def recreateManufacturing(self, item): """ 重新生成制造订单 """ if self.is_scrap is True: - sale_order = self.env['sale.order'].sudo().search([('name', '=', productions.origin)]) - values = self.env['mrp.production'].create_production1_values(self.production_id) - productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company( - self.production_id.company_id).create( - values) - # self.env['stock.move'].sudo().create(productions._get_moves_raw_values()) - self.env['stock.move'].sudo().create(productions._get_moves_finished_values()) - productions._create_workorder() - productions.filtered(lambda p: (not p.orderpoint_id and p.move_raw_ids) or \ - ( - p.move_dest_ids.procure_method != 'make_to_order' and - not p.move_raw_ids and not p.workorder_ids)).action_confirm() - for production_item in productions: - process_parameter_workorder = self.env['mrp.workorder'].search( - [('surface_technics_parameters_id', '!=', False), ('production_id', '=', production_item.id), - ('is_subcontract', '=', True)]) - if process_parameter_workorder: - is_pick = False - consecutive_workorders = [] - m = 0 - sorted_workorders = sorted(process_parameter_workorder, key=lambda w: w.id) - for i in range(len(sorted_workorders) - 1): - if m == 0: - is_pick = False - if sorted_workorders[i].supplier_id.id == sorted_workorders[i + 1].supplier_id.id and \ - sorted_workorders[i].is_subcontract == sorted_workorders[i + 1].is_subcontract and \ - sorted_workorders[i].id == sorted_workorders[i + 1].id - 1: - if sorted_workorders[i] not in consecutive_workorders: - consecutive_workorders.append(sorted_workorders[i]) - consecutive_workorders.append(sorted_workorders[i + 1]) - m += 1 - continue - else: - if m == len(consecutive_workorders) - 1 and m != 0: - self.env['stock.picking'].create_outcontract_picking(consecutive_workorders, - production_item) - if sorted_workorders[i] in consecutive_workorders: - is_pick = True - consecutive_workorders = [] - m = 0 - # 当前面的连续工序生成对应的外协出入库单再生成当前工序的外协出入库单 - if is_pick is False: - self.env['stock.picking'].create_outcontract_picking(sorted_workorders[i], - production_item) - if m == len(consecutive_workorders) - 1 and m != 0: - self.env['stock.picking'].create_outcontract_picking(consecutive_workorders, - production_item) - if sorted_workorders[i] in consecutive_workorders: - is_pick = True - consecutive_workorders = [] - m = 0 - if m == len(consecutive_workorders) - 1 and m != 0: - self.env['stock.picking'].create_outcontract_picking(consecutive_workorders, production_item) - if is_pick is False and m == 0: - if len(sorted_workorders) == 1: - self.env['stock.picking'].create_outcontract_picking(sorted_workorders, production_item) - else: - self.env['stock.picking'].create_outcontract_picking(sorted_workorders[i], production_item) - - for production in productions: - origin_production = production.move_dest_ids and production.move_dest_ids[ - 0].raw_material_production_id or False - orderpoint = production.orderpoint_id - if orderpoint and orderpoint.create_uid.id == SUPERUSER_ID and orderpoint.trigger == 'manual': - production.message_post( - body=_('This production order has been created from Replenishment Report.'), - message_type='comment', - subtype_xmlid='mail.mt_note') - elif orderpoint: - production.message_post_with_view( - 'mail.message_origin_link', - values={'self': production, 'origin': orderpoint}, - subtype_id=self.env.ref('mail.mt_note').id) - elif origin_production: - production.message_post_with_view( - 'mail.message_origin_link', - values={'self': production, 'origin': origin_production}, - subtype_id=self.env.ref('mail.mt_note').id) - - ''' - 创建生产计划 - ''' - # 工单耗时 - workorder_duration = 0 - for workorder in productions.workorder_ids: - workorder_duration += workorder.duration_expected - - if sale_order: - sale_order.mrp_production_ids |= productions - # sale_order.write({'schedule_status': 'to schedule'}) - self.env['sf.production.plan'].sudo().with_company(self.production_id.company_id).create({ - 'name': productions.name, - 'order_deadline': sale_order.deadline_of_delivery, - 'production_id': productions.id, - 'date_planned_start': productions.date_planned_start, - 'origin': productions.origin, - 'product_qty': productions.product_qty, - 'product_id': productions.product_id.id, - 'state': 'draft', - }) + procurement_requests = [] + sale_order = self.env['sale.order'].sudo().search([('name', '=', self.origin)]) + values = self.env['mrp.production'].create_production1_values(self) + # productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company( + # self.company_id).create( + # values) + # 查询出库移动记录 + out_picking = self.env['stock.picking'].search( + [('origin', '=', sale_order.name), ('name', 'ilike', 'WH/OUT/')]) + move = out_picking.move_ids.filtered(lambda pd: pd.product_id == self.product_id) + move_values = {'product_description_variants': '', + 'date_planned': fields.Datetime.now(), + 'date_deadline': fields.Datetime.now(), + 'move_dest_ids': move, + 'group_id': move.group_id, + 'route_ids': [], + 'warehouse_id': self.warehouse_id, + 'priority': 0, + 'orderpoint_id': False, + 'product_packaging_id': False} + procurement_requests.append(self.env['procurement.group'].Procurement( + move.product_id, 1.0, move.product_uom, + move.location_id, move.rule_id and move.rule_id.name or "/", + sale_order.name, move.company_id, move_values)) + self.env['procurement.group'].run(procurement_requests, + raise_user_error=not self.env.context.get('from_orderpoint')) + productions = self.env['mrp.production'].sudo().search( + [('origin', '=', self.origin)], order='id desc', limit=1) + move = self.env['stock.move'].search([('origin', '=', productions.name)], order='id desc') + for mo in move: + if mo.procure_method == 'make_to_order' and mo.name != productions.name: + if mo.name == '/': + domain = [('barcode', '=', 'WH-PC'), ('sequence_code', '=', 'PC')] + elif mo.name == '拉': + domain = [('barcode', '=', 'WH-INTERNAL'), ('sequence_code', '=', 'INT')] + picking_type = self.env['stock.picking.type'].search(domain) + mo.write({'picking_type_id': picking_type.id}) + mo._assign_picking() + else: + if mo.reference != productions.name: + mo.reference = productions.name + if mo.production_id: + if mo.production_id != productions: + mo.production_id = False + mo_move = self.env['stock.move'].search( + [('origin', '=', sale_order.name), ('reference', 'ilike', 'WH/MO/')]) + if mo_move: + sfp_move = self.env['stock.move'].search( + [('origin', '=', sale_order.name), ('reference', 'ilike', 'WH/SFP/')], limit=1) + mo_move.write({'reference': sfp_move.reference, 'partner_id': sfp_move.partner_id.id, + 'picking_id': sfp_move.picking_id.id, 'picking_type_id': sfp_move.picking_type_id.id, + 'production_id': False}) + productions.write({'programming_no': self.programming_no, 'is_remanufacture': True}) + # productions.procurement_group_id.mrp_production_ids.move_dest_ids.write( + # {'group_id': self.env['procurement.group'].search([('name', '=', sale_order.name)])}) + stock_picking = None + pc_picking = self.env['stock.picking'].search( + [('origin', '=', productions.name), ('name', 'ilike', 'WH/PC/')]) + stock_picking = pc_picking + int_picking = self.env['stock.picking'].search( + [('origin', '=', productions.name), ('name', 'ilike', 'WH/INT/')]) + stock_picking |= int_picking + for pick in stock_picking: + if pick.move_ids: + product_type_id = pick.move_ids[0].product_id.categ_id + if product_type_id.name == '坯料': + location_id = self.env['stock.location'].search([('name', '=', '坯料存货区')]) + if not location_id: + logging.info(f'没有搜索到【坯料存货区】: {location_id}') + break + if pick.picking_type_id.name == '内部调拨': + if pick.location_dest_id.product_type != product_type_id: + pick.location_dest_id = location_id.id + elif pick.picking_type_id.name == '生产发料': + if pick.location_id.product_type != product_type_id: + pick.location_id = location_id.id + scarp_process_parameter_workorder = self.env['mrp.workorder'].search( + [('surface_technics_parameters_id', '!=', False), ('production_id', '=', self.id), + ('is_subcontract', '=', True)]) + if scarp_process_parameter_workorder: + production_programming = self.env['mrp.production'].search( + [('programming_no', '=', self.programming_no), ('id', '!=', productions.id)], order='name asc') + production_list = [production.name for production in production_programming] + purchase_orders = self.env['purchase.order'].search([('origin', 'ilike', ','.join(production_list))]) + for purchase_item in purchase_orders.order_line: + for process_item in scarp_process_parameter_workorder: + if purchase_item.product_id.categ_type == '表面工艺': + if purchase_item.product_id.server_product_process_parameters_id == process_item.surface_technics_parameters_id: + print(purchase_orders.origin.find(productions.name)) + if purchase_orders.origin.find(productions.name) == -1: + purchase_orders.origin += ',' + productions.name + if item['is_reprogramming'] is False: + productions._create_workorder(item) + productions.programming_state = '已编程' + else: + productions.programming_state = '编程中' + return productions # 在之前的销售单上重新生成制造订单 - def create_production1_values(self, production, sale_order): + def create_production1_values(self, production): production_values_str = {'origin': production.origin, 'product_id': production.product_id.id, + 'programming_state': '已编程', 'product_description_variants': production.product_description_variants, 'product_qty': production.product_qty, 'product_uom_id': production.product_uom_id.id, @@ -1032,7 +1135,8 @@ class MrpProduction(models.Model): 'date_deadline': production.date_deadline, 'date_planned_start': production.date_planned_start, 'date_planned_finished': production.date_planned_finished, - 'procurement_group_id': sale_order.id, + # 'procurement_group_id': self.env["procurement.group"].create( + # {'name': production.name}).id, 'propagate_cancel': production.propagate_cancel, 'orderpoint_id': production.orderpoint_id.id, 'picking_type_id': production.picking_type_id.id, diff --git a/sf_manufacturing/models/mrp_routing_workcenter.py b/sf_manufacturing/models/mrp_routing_workcenter.py index 223ace85..0c380ebd 100644 --- a/sf_manufacturing/models/mrp_routing_workcenter.py +++ b/sf_manufacturing/models/mrp_routing_workcenter.py @@ -21,7 +21,7 @@ class ResMrpRoutingWorkcenter(models.Model): workcenter_ids = fields.Many2many('mrp.workcenter', 'rel_workcenter_route', required=True) bom_id = fields.Many2one('mrp.bom', required=False) surface_technics_id = fields.Many2one('sf.production.process', string="表面工艺") - + reserved_duration = fields.Float('预留时长', default=30, tracking=True) def get_no(self): international_standards = self.search( [('code', '!=', ''), ('active', 'in', [True, False])], diff --git a/sf_manufacturing/models/mrp_workcenter.py b/sf_manufacturing/models/mrp_workcenter.py index 64cf2d8d..03597980 100644 --- a/sf_manufacturing/models/mrp_workcenter.py +++ b/sf_manufacturing/models/mrp_workcenter.py @@ -41,6 +41,7 @@ class ResWorkcenter(models.Model): oee_target = fields.Float( string='OEE Target', help="Overall Effective Efficiency Target in percentage", default=90, tracking=True) + oee = fields.Float(compute='_compute_oee', help='Overall Equipment Effectiveness, based on the last month', store=True) time_start = fields.Float('Setup Time', tracking=True) time_stop = fields.Float('Cleanup Time', tracking=True) @@ -124,6 +125,8 @@ class ResWorkcenter(models.Model): res[wc_id] = [(datetime.fromtimestamp(s), datetime.fromtimestamp(e)) for s, e, _ in final_intervals_wc] return res + # AGV是否可配送 + is_agv_scheduling = fields.Boolean(string="AGV所属区域", tracking=True) class ResWorkcenterProductivity(models.Model): _inherit = 'mrp.workcenter.productivity' diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py index 8f045703..c3842d30 100644 --- a/sf_manufacturing/models/mrp_workorder.py +++ b/sf_manufacturing/models/mrp_workorder.py @@ -58,9 +58,15 @@ class ResMrpWorkOrder(models.Model): ('cancel', '取消')], string='Status', 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) - manual_quotation = fields.Boolean('人工编程', default=False, readonly=True) + @api.depends('production_id.manual_quotation') + def _compute_manual_quotation(self): + for item in self: + item.manual_quotation = item.production_id.manual_quotation + + manual_quotation = fields.Boolean('人工编程', default=False, compute=_compute_manual_quotation, store=True) def _compute_working_users(self): super()._compute_working_users() @@ -131,13 +137,32 @@ class ResMrpWorkOrder(models.Model): is_subcontract = fields.Boolean(string='是否外协') surface_technics_parameters_id = fields.Many2one('sf.production.process.parameter', string="表面工艺可选参数") picking_ids = fields.Many2many('stock.picking', string='外协出入库单') + # purchase_id = fields.Many2one('purchase.order', string='外协采购单') surface_technics_picking_count = fields.Integer("外协出入库", compute='_compute_surface_technics_picking_ids') + surface_technics_purchase_count = fields.Integer("外协采购", compute='_compute_surface_technics_purchase_ids') + + # 是否绑定托盘 + is_trayed = fields.Boolean(string='是否绑定托盘', default=False) @api.depends('name', 'production_id.name') def _compute_surface_technics_picking_ids(self): - for order in self: - picking_ids = self.env['stock.picking'].search([('id', 'in', order.picking_ids.ids)]) - order.surface_technics_picking_count = len(picking_ids) + for workorder in self: + if workorder.routing_type == '表面工艺': + domain = [('origin', '=', workorder.production_id.name)] + previous_workorder = self.env['mrp.workorder'].search( + [('sequence', '=', workorder.sequence - 1), ('routing_type', '=', '表面工艺'), + ('production_id', '=', workorder.production_id.id)]) + if previous_workorder: + process_product = self.env['product.template']._get_process_parameters_product( + previous_workorder.surface_technics_parameters_id) + domain += [('partner_id', '=', process_product.partner_id.id)] + else: + domain += [('surface_technics_parameters_id', '=', workorder.surface_technics_parameters_id.id)] + picking_ids = self.env['stock.picking'].search(domain, order='id asc') + workorder.surface_technics_picking_count = len(picking_ids) + workorder.picking_ids = picking_ids.ids + else: + workorder.surface_technics_picking_count = 0 def action_view_surface_technics_picking(self): self.ensure_one() @@ -153,6 +178,38 @@ class ResMrpWorkOrder(models.Model): action['context'] = dict(self._context, default_origin=self.name) return action + @api.depends('state', 'production_id.name') + def _compute_surface_technics_purchase_ids(self): + for order in self: + if order.routing_type == '表面工艺': + production_programming = self.env['mrp.production'].search( + [('programming_no', '=', order.production_id.programming_no)], order='name asc') + production_no_remanufacture = production_programming.filtered(lambda a: a.is_remanufacture is False) + production_list = [production.name for production in production_programming] + purchase = self.env['purchase.order'].search([('origin', '=', ','.join(production_list))]) + for line in purchase.order_line: + if line.product_id.server_product_process_parameters_id == order.surface_technics_parameters_id and line.product_qty == len( + production_no_remanufacture): + order.surface_technics_purchase_count = len(purchase) + else: + order.surface_technics_purchase_count = 0 + + def action_view_surface_technics_purchase(self): + self.ensure_one() + production_programming = self.env['mrp.production'].search( + [('programming_no', '=', self.production_id.programming_no)], order='name asc') + production_list = [production.name for production in production_programming] + purchase_orders = self.env['purchase.order'].search([('origin', '=', ','.join(production_list))]) + result = { + "type": "ir.actions.act_window", + "res_model": "purchase.order", + "res_id": purchase_orders.id, + # "domain": [['id', 'in', self.purchase_id]], + "name": _("Purchase Orders"), + 'view_mode': 'form', + } + return result + supplier_id = fields.Many2one('res.partner', string='外协供应商') equipment_id = fields.Many2one('maintenance.equipment', string='加工设备', tracking=True) # 保存名称 @@ -181,6 +238,7 @@ class ResMrpWorkOrder(models.Model): tool_state = fields.Selection([('0', '正常'), ('1', '缺刀'), ('2', '无效刀')], string='功能刀具状态', default='0', store=True, compute='_compute_tool_state') tool_state_remark = fields.Text(string='功能刀具状态备注(缺刀)', compute='_compute_tool_state_remark', store=True) + reserved_duration = fields.Float('预留时长', default=30, tracking=True) @api.depends('cnc_ids.tool_state') def _compute_tool_state_remark(self): @@ -318,10 +376,10 @@ class ResMrpWorkOrder(models.Model): vals['leave_id'] = leave.id self.write(vals) - @api.onchange('rfid_code') - def _onchange(self): - if self.rfid_code and self.state == 'progress': - self.workpiece_delivery_ids[0].write({'rfid_code': self.rfid_code}) + # @api.onchange('rfid_code') + # def _onchange(self): + # if self.rfid_code and self.state == 'progress': + # self.workpiece_delivery_ids[0].write({'rfid_code': self.rfid_code}) def get_plan_workorder(self, production_line): tomorrow = (date.today() + timedelta(days=+1)).strftime("%Y-%m-%d") @@ -590,29 +648,36 @@ class ResMrpWorkOrder(models.Model): # 拼接工单对象属性值 def json_workorder_str(self, k, production, route, item): # 计算预计时长duration_expected - if route.routing_type == '切割': - duration_expected = self.env['mrp.routing.workcenter'].sudo().search( - [('name', '=', '切割')]).time_cycle - # elif route.routing_type == '获取CNC加工程序': + routing_types = ['切割', '装夹预调', 'CNC加工', '解除装夹'] + if route.routing_type in routing_types: + routing_workcenter = self.env['mrp.routing.workcenter'].sudo().search( + [('name', '=', route.routing_type)]) + duration_expected = routing_workcenter.time_cycle + reserved_duration = routing_workcenter.reserved_duration + # if route.routing_type == '切割': # duration_expected = self.env['mrp.routing.workcenter'].sudo().search( - # [('name', '=', '获取CNC加工程序')]).time_cycle - elif route.routing_type == '装夹预调': - duration_expected = self.env['mrp.routing.workcenter'].sudo().search( - [('name', '=', '装夹预调')]).time_cycle - # elif route.routing_type == '前置三元定位检测': + # [('name', '=', '切割')]).time_cycle + # # elif route.routing_type == '获取CNC加工程序': + # # duration_expected = self.env['mrp.routing.workcenter'].sudo().search( + # # [('name', '=', '获取CNC加工程序')]).time_cycle + # elif route.routing_type == '装夹预调': # duration_expected = self.env['mrp.routing.workcenter'].sudo().search( - # [('name', '=', '前置三元定位检测')]).time_cycle - elif route.routing_type == 'CNC加工': - duration_expected = self.env['mrp.routing.workcenter'].sudo().search( - [('name', '=', 'CNC加工')]).time_cycle - # elif route.routing_type == '后置三元质量检测': + # [('name', '=', '装夹预调')]).time_cycle + # # elif route.routing_type == '前置三元定位检测': + # # duration_expected = self.env['mrp.routing.workcenter'].sudo().search( + # # [('name', '=', '前置三元定位检测')]).time_cycle + # elif route.routing_type == 'CNC加工': # duration_expected = self.env['mrp.routing.workcenter'].sudo().search( - # [('name', '=', '后置三元质量检测')]).time_cycle - elif route.routing_type == '解除装夹': - duration_expected = self.env['mrp.routing.workcenter'].sudo().search( - [('name', '=', '解除装夹')]).time_cycle + # [('name', '=', 'CNC加工')]).time_cycle + # # elif route.routing_type == '后置三元质量检测': + # # duration_expected = self.env['mrp.routing.workcenter'].sudo().search( + # # [('name', '=', '后置三元质量检测')]).time_cycle + # elif route.routing_type == '解除装夹': + # duration_expected = self.env['mrp.routing.workcenter'].sudo().search( + # [('name', '=', '解除装夹')]).time_cycle else: duration_expected = 60 + reserved_duration = 30 workorders_values_str = [0, '', { 'product_uom_id': production.product_uom_id.id, 'qty_producing': 0, @@ -634,25 +699,36 @@ class ResMrpWorkOrder(models.Model): k, item), 'cmm_ids': False if route.routing_type != 'CNC加工' else self.env['sf.cmm.program']._json_cmm_program(k, item), - 'workpiece_delivery_ids': False if not route.routing_type == '装夹预调' else self._json_workpiece_delivery_list( - production) + # 'workpiece_delivery_ids': False if not route.routing_type == '装夹预调' else self._json_workpiece_delivery_list( + # production) + 'reserved_duration': reserved_duration, }] return workorders_values_str - def _json_workpiece_delivery_list(self, production): - up_route = self.env['sf.agv.task.route'].search([('route_type', '=', '上产线')], limit=1, order='id asc') - down_route = self.env['sf.agv.task.route'].search([('route_type', '=', '下产线')], limit=1, order='id asc') + def _json_workpiece_delivery_list(self): + # 修改在装夹工单完成后,生成上产线的工件配送单 + + # up_route = self.env['sf.agv.task.route'].search([('route_type', '=', '上产线')], limit=1, order='id asc') + # down_route = self.env['sf.agv.task.route'].search([('route_type', '=', '下产线')], limit=1, order='id asc') return [ [0, '', - {'production_id': production.id, 'production_line_id': production.production_line_id.id, 'type': '上产线', - 'route_id': up_route.id, - 'feeder_station_start_id': up_route.start_site_id.id, - 'feeder_station_destination_id': up_route.end_site_id.id}], - [0, '', - {'production_id': production.id, 'production_line_id': production.production_line_id.id, 'type': '下产线', - 'route_id': down_route.id, - 'feeder_station_start_id': down_route.start_site_id.id, - 'feeder_station_destination_id': down_route.end_site_id.id}]] + { + 'production_id': self.production_id.id, + 'production_line_id': self.production_id.production_line_id.id, + 'type': '上产线', + 'is_cnc_program_down': True, + 'rfid_code': self.rfid_code + # 'route_id': up_route.id, + # 'feeder_station_start_id': agv_start_site_id, + # 'feeder_station_destination_id': up_route.end_site_id.id + } + ], + # [0, '', + # {'production_id': production.id, 'production_line_id': production.production_line_id.id, 'type': '下产线', + # 'route_id': down_route.id, + # 'feeder_station_start_id': down_route.start_site_id.id, + # 'feeder_station_destination_id': down_route.end_site_id.id}] + ] # 拼接工单对象属性值(表面工艺) def _json_workorder_surface_process_str(self, production, route, process_parameter, supplier_id): @@ -868,7 +944,7 @@ class ResMrpWorkOrder(models.Model): workorder.state = 'waiting' elif workorder.routing_type == '解除装夹' and workorder.state not in ['done', 'rework', 'cancel']: if cnc_workorder: - if not cnc_workorder_pending: + if not cnc_workorder_pending or unclamp_workorder.test_results == '报废': workorder.state = 'waiting' # else: # if workorder.production_id.is_rework is True: @@ -885,10 +961,26 @@ class ResMrpWorkOrder(models.Model): # workorder.state = 'ready' if workorder.routing_type == '表面工艺' and workorder.state not in ['done', 'progress']: if unclamp_workorder: - workorder.state = 'ready' - # else: - # if workorder.state not in ['cancel', 'rework']: - # workorder.state = 'rework' + if workorder.is_subcontract is False: + workorder.state = 'ready' + else: + production_programming = self.env['mrp.production'].search( + [('programming_no', '=', self.production_id.programming_no)], order='name asc') + production_no_remanufacture = production_programming.filtered( + lambda a: a.is_remanufacture is False) + production_list = [production.name for production in production_programming] + purchase_orders = self.env['purchase.order'].search( + [('origin', 'ilike', ','.join(production_list))]) + for line in purchase_orders.order_line: + if line.product_id.server_product_process_parameters_id == workorder.surface_technics_parameters_id and line.product_qty == len( + production_no_remanufacture): + if purchase_orders.state == 'purchase': + workorder.state = 'ready' + else: + workorder.state = 'waiting' + elif workorder.production_id.state == 'scrap': + if workorder.routing_type == '解除装夹' and unclamp_workorder.test_results == '报废': + workorder.state = 'waiting' if workorder.routing_type == '装夹预调' and workorder.state in ['waiting', 'ready', 'pending']: workorder_ids = workorder.production_id.workorder_ids work_bo = True @@ -1007,7 +1099,7 @@ class ResMrpWorkOrder(models.Model): ('location_dest_id', '=', self.env['stock.location'].search( [('barcode', 'ilike', 'VL-SPOC')]).id), ('origin', '=', self.production_id.name)]) - if move_out: + if move_out.state != 'done': move_out.write({'state': 'assigned'}) self.env['stock.move.line'].create(move_out.get_move_line(self.production_id, self)) @@ -1074,23 +1166,31 @@ class ResMrpWorkOrder(models.Model): def button_finish(self): for record in self: if record.routing_type == '装夹预调': - if not record.material_center_point and record.X_deviation_angle > 0: - raise UserError("请对前置三元检测定位参数进行计算定位") if not record.rfid_code and record.is_rework is False: raise UserError("请扫RFID码进行绑定") + if record.is_rework is False: + if not record.material_center_point and record.X_deviation_angle > 0: + raise UserError("坯料中心点为空或X偏差角度小于等于0") record.process_state = '待加工' # record.write({'process_state': '待加工'}) record.production_id.process_state = '待加工' + # 生成工件配送单 + record.workpiece_delivery_ids = record._json_workpiece_delivery_list() if record.routing_type == 'CNC加工': record.process_state = '待解除装夹' # record.write({'process_state': '待加工'}) record.production_id.process_state = '待解除装夹' + self.env['sf.production.plan'].sudo().search([('name', '=', record.production_id.name)]).write({ + 'state': 'finished', + 'actual_end_time': datetime.now() + }) record.production_id.write({'detection_result_ids': [(0, 0, { 'rework_reason': record.reason, 'detailed_reason': record.detailed_reason, 'processing_panel': record.processing_panel, 'routing_type': record.routing_type, - 'handle_result': '待处理' if record.test_results == '返工' or record.is_rework is True else '', + 'handle_result': '待处理' if record.test_results in ['返工', + '报废'] or record.is_rework is True else '', 'test_results': record.test_results, 'test_report': record.detection_report})], 'is_scrap': True if record.test_results == '报废' else False}) @@ -1110,28 +1210,13 @@ class ResMrpWorkOrder(models.Model): picking_out = record.env['stock.move.line'].search( [('picking_id', '=', record.picking_ids[0].id)]) logging.info('picking_out:%s' % picking_out.picking_id.name) - if picking_out: - order_line_ids = [] - logging.info('surface_technics_parameters_id:%s' % record.surface_technics_parameters_id.name) - server_product = self.env['product.template'].search( - [('server_product_process_parameters_id', '=', record.surface_technics_parameters_id.id), - ('detailed_type', '=', 'service')]) - logging.info('server_product:%s' % server_product.name) - if server_product: - order_line_ids.append((0, 0, { - 'product_id': server_product.product_variant_id.id, - 'product_qty': 1, - 'product_uom': server_product.uom_id.id - })) - self.env['purchase.order'].sudo().create({ - 'partner_id': server_product.seller_ids.partner_id.id, - 'origin': record.production_id.name, - 'state': 'draft', - 'order_line': order_line_ids, - }) - else: - raise UserError( - '请先在产品中配置表面工艺为%s相关的外协服务产品' % item.surface_technics_parameters_id.name) + # if picking_out: + # order_line_ids = [] + # logging.info('surface_technics_parameters_id:%s' % record.surface_technics_parameters_id.name) + # + # else: + # raise UserError( + # '请先在产品中配置表面工艺为%s相关的外协服务产品' % item.surface_technics_parameters_id.name) tem_date_planned_finished = record.date_planned_finished tem_date_finished = record.date_finished logging.info('routing_type:%s' % record.routing_type) @@ -1183,6 +1268,17 @@ class ResMrpWorkOrder(models.Model): record.production_id.button_mark_done1() # record.production_id.state = 'done' + # 解绑托盘 + def unbind_tray(self): + self.production_id.workorder_ids.write({ + 'rfid_code': False, + 'tray_serial_number': False, + 'tray_product_id': False, + 'tray_brand_id': False, + 'tray_type_id': False, + 'tray_model_id': False, + 'is_trayed': False}) + # 将FTP的检测报告文件下载到临时目录 def download_reportfile_tmp(self, workorder, reportpath): logging.info('reportpath/ftp地址:%s' % reportpath) @@ -1222,6 +1318,66 @@ class ResMrpWorkOrder(models.Model): else: raise UserError("无关联制造订单或关联序列号,无法打印。请检查!") + @api.model + def get_views(self, views, options=None): + res = super().get_views(views, options) + if res['views'].get('list', {}) and self.env.context.get('search_default_workcenter_id'): + workcenter = self.env['mrp.workcenter'].browse(self.env.context.get('search_default_workcenter_id')) + tree_view = res['views']['list'] + if workcenter.name == '工件拆卸中心': + arch = etree.fromstring(tree_view['arch']) + # 查找 tree 标签 + tree_element = arch.xpath("//tree")[0] + + # 查找或创建 header 标签 + header_element = tree_element.find('header') + if header_element is None: + header_element = etree.Element('header') + tree_element.insert(0, header_element) + + # 创建并添加按钮元素 + button_element = etree.Element('button', { + 'name': 'button_delivery', + 'type': 'object', + 'string': '解除装夹', + 'class': 'btn-primary', + # 'className': 'btn-primary', + 'modifiers': '{"force_show": 1}' + }) + header_element.append(button_element) + + # 更新 tree_view 的 arch + tree_view['arch'] = etree.tostring(arch, encoding='unicode') + return res + + def button_delivery(self): + production_ids = [] + workorder_ids = [] + delivery_type = '运送空料架' + max_num = 4 # 最大配送数量 + if len(self) > max_num: + raise UserError('仅限于拆卸1-4个制造订单,请重新选择') + for item in self: + if item.state != 'ready': + raise UserError('请选择状态为【就绪】的工单进行解除装夹') + + production_ids.append(item.production_id.id) + workorder_ids.append(item.id) + return { + 'name': _('确认'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'sf.workpiece.delivery.wizard', + 'target': 'new', + 'context': { + # 'default_delivery_ids': [(6, 0, delivery_ids)], + 'default_production_ids': [(6, 0, production_ids)], + 'default_delivery_type': delivery_type, + 'default_workorder_ids': [(6, 0, workorder_ids)], + 'default_workcenter_id': self.env.context.get('default_workcenter_id'), + 'default_confirm_button': '确认解除' + }} + class CNCprocessing(models.Model): _name = 'sf.cnc.processing' @@ -1430,6 +1586,7 @@ class SfWorkOrderBarcodes(models.Model): raise UserError('该Rfid【%s】绑定的是【%s】, 不是托盘!!!' % (barcode, lot.product_id.name)) self.process_state = '待检测' self.date_start = datetime.now() + self.is_trayed = True else: raise UserError('没有找到Rfid为【%s】的托盘信息!!!' % barcode) # stock_move_line = self.env['stock.move.line'].search([('lot_name', '=', barcode)]) @@ -1501,20 +1658,24 @@ class WorkPieceDelivery(models.Model): feeder_station_destination_id = fields.Many2one('sf.agv.site', '目的接驳站') task_delivery_time = fields.Datetime('任务下发时间') task_completion_time = fields.Datetime('任务完成时间') - type = fields.Selection( - [('上产线', '上产线'), ('下产线', '下产线'), ('运送空料架', '运送空料架')], string='类型') + + def _get_agv_route_type_selection(self): + return self.env['sf.agv.task.route'].fields_get(['route_type'])['route_type']['selection'] + + type = fields.Selection(selection=_get_agv_route_type_selection, string='类型') delivery_duration = fields.Float('配送时长', compute='_compute_delivery_duration') status = fields.Selection( - [('待下发', '待下发'), ('待配送', '待配送'), ('已配送', '已配送'), ('已取消', '已取消')], string='状态', - default='待下发', - tracking=True) + [('待下发', '待下发'), ('已下发', '待配送'), ('已配送', '已配送'), ('已取消', '已取消')], string='状态', + default='待下发', tracking=True) is_cnc_program_down = fields.Boolean('程序是否下发', default=False, tracking=True) is_manual_work = fields.Boolean('人工操作', default=False) active = fields.Boolean(string="有效", default=True) + agv_scheduling_id = fields.Many2one('sf.agv.scheduling', 'AGV任务调度') + @api.model def create(self, vals): - if vals['route_id'] and vals.get('type') is None: + if vals.get('route_id') and vals.get('type') is None: vals['type'] = '运送空料架' else: if vals.get('name', '/') == '/' or vals.get('name', '/') is False: @@ -1526,14 +1687,14 @@ class WorkPieceDelivery(models.Model): obj.feeder_station_start_id.name, obj.feeder_station_destination_id.name) return obj - @api.constrains('route_id') - def _check_route_id(self): - if self.type == '运送空料架': - if self.route_id and self.name is False: - route = self.sudo().search( - [('route_id', '=', self.route_id.id), ('id', '!=', self.id), ('name', 'ilike', '运送空料架路线')]) - if route: - raise UserError("该任务路线已存在,请重新选择") + # @api.constrains('route_id') + # def _check_route_id(self): + # if self.type == '运送空料架': + # if self.route_id and self.name is False: + # route = self.sudo().search( + # [('route_id', '=', self.route_id.id), ('id', '!=', self.id), ('name', 'ilike', '运送空料架路线')]) + # if route: + # raise UserError("该任务路线已存在,请重新选择") # @api.constrains('name') # def _check_name(self): @@ -1562,84 +1723,44 @@ class WorkPieceDelivery(models.Model): def button_delivery(self): delivery_ids = [] production_ids = [] + workorder_ids = [] is_cnc_down = 0 is_not_production_line = 0 - is_not_route = 0 same_production_line_id = None - same_route_id = None - down_status = '待下发' - production_type = None - num = 0 + delivery_type = '上产线' + max_num = 4 # 最大配送数量 + if len(self) > max_num: + raise UserError('仅限于配送1-4个制造订单,请重新选择') for item in self: - num += 1 - if production_type is None: - production_type = item.type - if item.type == "运送空料架": - if num >= 2: - raise UserError('仅选择一条路线进行配送,请重新选择') - else: - delivery_ids.append(item.id) - else: - if num > 4: - raise UserError('仅限于配送1-4个制造订单,请重新选择') - if item.status in ['待配送', '已配送']: - raise UserError('请选择状态为【待下发】的制造订单进行配送') - if item.route_id: - if same_route_id is None: - same_route_id = item.route_id.id - if item.route_id.id != same_route_id: - is_not_route += 1 - # else: - # raise UserError('请选择【任务路线】再进行配送') - # if item.production_id.production_line_state == '已下产线' and item.state == '待下发' and item.type == '下产线': - # raise UserError('该制造订单已下产线,无需配送') - if production_type != item.type: - raise UserError('请选择类型为%s的制造订单进行配送' % production_type) - if down_status != item.status: - up_workpiece = self.search([('type', '=', '上产线'), ('production_id', '=', item.production_id), - ('status', '=', '待下发')]) - if up_workpiece: - raise UserError('您所选择的制造订单暂未上产线,请在上产线后再进行配送') - else: - raise UserError('请选择状态为【待下发】的制造订单进行配送') - - if same_production_line_id is None: - same_production_line_id = item.production_line_id.id - if item.production_line_id.id != same_production_line_id: - is_not_production_line += 1 - if item.is_cnc_program_down is False: - is_cnc_down += 1 - if is_cnc_down == 0 and is_not_production_line == 0 and is_not_route == 0: - delivery_ids.append(item.id) - production_ids.append(item.production_id.id) + if item.status != '待下发': + raise UserError('请选择状态为【待下发】的制造订单进行配送') + if same_production_line_id is None: + same_production_line_id = item.production_line_id.id + if item.production_line_id.id != same_production_line_id: + is_not_production_line += 1 + if item.is_cnc_program_down is False: + is_cnc_down += 1 + if is_cnc_down == 0 and is_not_production_line == 0: + delivery_ids.append(item.id) + production_ids.append(item.production_id.id) + workorder_ids.append(item.workorder_id.id) if is_cnc_down >= 1: raise UserError('您所选择制造订单的【CNC程序】暂未下发,请在程序下发后再进行配送') if is_not_production_line >= 1: raise UserError('您所选择制造订单的【目的生产线】不一致,请重新确认') - if is_not_route >= 1: - raise UserError('您所选择制造订单的【任务路线】不一致,请重新确认') - is_free = self._check_avgsite_state() - if is_free is True: - if delivery_ids: - return { - 'name': _('确认'), - 'type': 'ir.actions.act_window', - 'view_mode': 'form', - 'res_model': 'sf.workpiece.delivery.wizard', - 'target': 'new', - 'context': { - 'default_delivery_ids': [(6, 0, delivery_ids)], - 'default_production_ids': [(6, 0, production_ids)], - 'default_destination_production_line_id': same_production_line_id, - 'default_route_id': same_route_id, - 'default_type': production_type, - }} - else: - if production_type == '运送空料架': - raise UserError("您所选择的【任务路线】的【终点接驳站】已占用,请在该接驳站空闲时进行配送") - else: - raise UserError( - "您所选择制造订单的【任务路线】的【终点接驳站】已占用,请在该接驳站空闲时或选择其他路线进行配送") + return { + 'name': _('确认'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'sf.workpiece.delivery.wizard', + 'target': 'new', + 'context': { + 'default_delivery_ids': [(6, 0, delivery_ids)], + 'default_production_ids': [(6, 0, production_ids)], + 'default_delivery_type': delivery_type, + 'default_workorder_ids': [(6, 0, workorder_ids)], + 'default_confirm_button': '确认配送' + }} # 验证agv站点是否可用 def _check_avgsite_state(self): diff --git a/sf_manufacturing/models/product_template.py b/sf_manufacturing/models/product_template.py index 4d05de81..071f1167 100644 --- a/sf_manufacturing/models/product_template.py +++ b/sf_manufacturing/models/product_template.py @@ -5,8 +5,10 @@ import base64 import hashlib import os from odoo import models, fields, api, _ -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, UserError from odoo.modules import get_resource_path + + from OCC.Extend.DataExchange import read_step_file from OCC.Extend.DataExchange import write_stl_file @@ -106,6 +108,15 @@ class ResProductMo(models.Model): name = fields.Char('产品名称', compute='_compute_tool_name', store=True, required=False) + @api.constrains('seller_ids') + def _check_seller_ids(self): + if self.categ_type == '表面工艺': + if self.seller_ids: + if self.seller_ids[0].price == 0.0: + raise UserError("请在该产品【采购】中的【价格】进行输入") + else: + raise UserError("请在【采购】中输入供应商信息") + @api.depends('cutting_tool_model_id', 'specification_id') def _compute_tool_name(self): for item in self: @@ -113,6 +124,10 @@ class ResProductMo(models.Model): name = '%s%s' % (item.cutting_tool_model_id.name, item.specification_id.name) item.name = name + def _get_process_parameters_product(self, production_process): + return self.env['product.template'].search( + [('server_product_process_parameters_id', '=', production_process.id)]).seller_ids[0] + @api.onchange('cutting_tool_model_id') def _onchange_cutting_tool_model_id(self): for item in self: @@ -640,6 +655,10 @@ class ResProductMo(models.Model): 'part_number': item.get('part_number') or '', 'active': True, } + tax_id = self.env['account.tax'].sudo().search( + [('type_tax_use', '=', 'sale'), ('amount', '=', item.get('tax')), ('price_include', '=', 'True')]) + if tax_id: + vals.update({'taxes_id': [(6, 0, [int(tax_id)])]}) copy_product_id.sudo().write(vals) product_id.product_tmpl_id.active = False return copy_product_id @@ -736,7 +755,11 @@ class ResProductMo(models.Model): # 产品名称唯一性校验 for item in templates: if len(self.search([('name', '=', item.name)])) > 1: - raise ValidationError('产品名称【%s】已存在' % item.name) + raise UserError('产品名称【%s】已存在' % item.name) + if item.categ_type == '表面工艺': + if len(self.search([('server_product_process_parameters_id', '=', + item.server_product_process_parameters_id.id)])) > 1: + raise UserError('表面工艺参数为【%s】的产品已存在' % item.server_product_process_parameters_id.name) if "create_product_product" not in self._context: templates._create_variant_ids() @@ -800,7 +823,7 @@ class ResProductFixture(models.Model): diameter = fields.Float('直径(mm)', digits=(16, 2)) # '零点卡盘' 字段 - weight = fields.Float('重量(mm)', digits=(16, 2)) + weight = fields.Float('重量(kg)', digits=(16, 2)) orientation_dish_diameter = fields.Float('定位盘直径(mm)', digits=(16, 2)) clamping_diameter = fields.Float('装夹直径(mm)', digits=(16, 2)) clamping_num = fields.Selection([('1', '1'), ('2', '2'), ('4', '4'), ('6', '6'), ('8', '8')], string='装夹单元数') @@ -947,6 +970,7 @@ class SfMaintenanceEquipmentAndProductTemplate(models.Model): raise ValidationError("机床基坐标获取失败") + class SfMaintenanceEquipmentTool(models.Model): _name = 'maintenance.equipment.tool' _description = '机床刀位' diff --git a/sf_manufacturing/models/res_config_setting.py b/sf_manufacturing/models/res_config_setting.py new file mode 100644 index 00000000..d6b029c6 --- /dev/null +++ b/sf_manufacturing/models/res_config_setting.py @@ -0,0 +1,22 @@ +from odoo import models, fields, api + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + is_agv_task_dispatch = fields.Boolean('是否下发AGV任务', default=False) + + @api.model + def get_values(self): + values = super(ResConfigSettings, self).get_values() + config = self.env['ir.config_parameter'].sudo() + is_agv_task_dispatch = config.get_param('is_agv_task_dispatch') + values.update( + is_agv_task_dispatch=is_agv_task_dispatch, + ) + return values + + def set_values(self): + super(ResConfigSettings, self).set_values() + config = self.env['ir.config_parameter'].sudo() + config.set_param("is_agv_task_dispatch", self.is_agv_task_dispatch or False) diff --git a/sf_manufacturing/models/stock.py b/sf_manufacturing/models/stock.py index 4bf7cc72..b9483a17 100644 --- a/sf_manufacturing/models/stock.py +++ b/sf_manufacturing/models/stock.py @@ -68,6 +68,7 @@ class StockRule(models.Model): @api.model def _run_pull(self, procurements): + logging.info(procurements) moves_values_by_company = defaultdict(list) mtso_products_by_locations = defaultdict(list) @@ -168,7 +169,6 @@ class StockRule(models.Model): else: forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id] -= qty_needed procure_method = 'make_to_stock' - move_values = rule._get_stock_move_values(*procurement) move_values['procure_method'] = procure_method moves_values_by_company[procurement.company_id.id].append(move_values) @@ -176,10 +176,10 @@ class StockRule(models.Model): for company_id, moves_values in moves_values_by_company.items(): # create the move as SUPERUSER because the current user may not have the rights to do it (mto product # launched by a sale for example) - moves = self.env['stock.move'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(moves_values) + moves = self.env['stock.move'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create( + moves_values) # Since action_confirm launch following procurement_group we should activate it. moves._action_confirm() - return True @api.model @@ -217,6 +217,23 @@ class StockRule(models.Model): ( p.move_dest_ids.procure_method != 'make_to_order' and not p.move_raw_ids and not p.workorder_ids)).action_confirm() + # 处理 根据制造订单生成的采购单坯料入库时到原材料库,手动将原材料位置该为坯料存货区 + for production in productions: + if production.picking_ids: + product_type_id = production.picking_ids[0].move_ids[0].product_id.categ_id + if product_type_id.name == '坯料': + location_id = self.env['stock.location'].search([('name', '=', '坯料存货区')]) + if not location_id: + logging.info(f'没有搜索到【坯料存货区】: {location_id}') + break + for picking_id in production.picking_ids: + if picking_id.picking_type_id.name == '内部调拨': + if picking_id.location_dest_id.product_type != product_type_id: + picking_id.location_dest_id = location_id.id + elif picking_id.picking_type_id.name == '生产发料': + if picking_id.location_id.product_type != product_type_id: + picking_id.location_id = location_id.id + for production in productions: ''' 创建制造订单时生成序列号 @@ -271,14 +288,70 @@ class StockRule(models.Model): # 为同一个product_id创建一个生产订单名称列表 product_id_to_production_names[product_id] = [production.name for production in all_production] for production_item in productions: + production_programming = self.env['mrp.production'].search( + [('product_id.id', '=', production_item.product_id.id), + ('origin', '=', production_item.origin)], + limit=1, order='id asc') if production_item.product_id.id in product_id_to_production_names: + if not production_programming.programming_no: + if production_item.product_id.model_process_parameters_ids: + is_purchase = False + sorted_process_parameters = sorted(production_item.product_id.model_process_parameters_ids, + key=lambda w: w.id) + + consecutive_process_parameters = [] + m = 0 + for i in range(len(sorted_process_parameters) - 1): + if m == 0: + is_purchase = False + if self.env['product.template']._get_process_parameters_product( + sorted_process_parameters[i]).partner_id == self.env[ + 'product.template']._get_process_parameters_product(sorted_process_parameters[ + i + 1]).partner_id and \ + sorted_process_parameters[i].gain_way == '外协': + if sorted_process_parameters[i] not in consecutive_process_parameters: + consecutive_process_parameters.append(sorted_process_parameters[i]) + consecutive_process_parameters.append(sorted_process_parameters[i + 1]) + m += 1 + continue + else: + if m == len(consecutive_process_parameters) - 1 and m != 0: + self.env['purchase.order'].get_purchase_order(consecutive_process_parameters, + production_item, + product_id_to_production_names) + if sorted_process_parameters[i] in consecutive_process_parameters: + is_purchase = True + consecutive_process_parameters = [] + m = 0 + # 当前面的连续外协采购单生成再生成当前外协采购单 + if is_purchase is False: + self.env['purchase.order'].get_purchase_order(consecutive_process_parameters, + production_item, + product_id_to_production_names) + if m == len(consecutive_process_parameters) - 1 and m != 0: + self.env['purchase.order'].get_purchase_order(consecutive_process_parameters, + production_item, + product_id_to_production_names) + if sorted_process_parameters[i] in consecutive_process_parameters: + is_purchase = True + consecutive_process_parameters = [] + m = 0 + if m == len(consecutive_process_parameters) - 1 and m != 0: + self.env['purchase.order'].get_purchase_order(consecutive_process_parameters, + production_item, + product_id_to_production_names) + if is_purchase is False and m == 0: + if len(sorted_process_parameters) == 1: + self.env['purchase.order'].get_purchase_order(sorted_process_parameters, + production_item, + product_id_to_production_names) + else: + self.env['purchase.order'].get_purchase_order(sorted_process_parameters[i], + production_item, + product_id_to_production_names) # # 同一个产品多个制造订单对应一个编程单和模型库 # # 只调用一次fetchCNC,并将所有生产订单的名称作为字符串传递 if not production_item.programming_no: - production_programming = self.env['mrp.production'].search( - [('product_id.id', '=', production_item.product_id.id), - ('origin', '=', production_item.origin)], - limit=1, order='id asc') if not production_programming.programming_no: production_item.fetchCNC( ', '.join(product_id_to_production_names[production_item.product_id.id])) @@ -368,7 +441,7 @@ class ProductionLot(models.Model): if product.tracking == "serial": last_serial = self.env['stock.lot'].search( [('company_id', '=', company.id), ('product_id', '=', product.id)], - limit=1, order='id DESC') + limit=1, order='name desc') if last_serial: if product.categ_id.name == '刀具': return self.env['stock.lot'].get_tool_generate_lot_names1(company, product) @@ -468,12 +541,11 @@ class ProductionLot(models.Model): class StockPicking(models.Model): _inherit = 'stock.picking' - # workorder_in_id = fields.One2many('mrp.workorder', 'picking_in_id') - # workorder_out_id = fields.One2many('mrp.workorder', 'picking_out_id') + surface_technics_parameters_id = fields.Many2one('sf.production.process.parameter', string="表面工艺可选参数") # 设置外协出入单的名称 def _get_name_Res(self, rescode): - last_picking = self.sudo().search([('name', 'like', rescode)], order='create_date desc,id desc', limit=1) + last_picking = self.sudo().search([('name', 'ilike', rescode)], order='create_date desc,id desc', limit=1) if not last_picking: num = "%04d" % 1 else: @@ -499,7 +571,7 @@ class StockPicking(models.Model): [('barcode', 'ilike', 'WH-PREPRODUCTION')]).id), ('location_id', '=', self.env['stock.location'].search( [('barcode', 'ilike', 'VL-SPOC')]).id), - ('origin', '=', self.origin)]) + ('origin', '=', self.origin), ('picking_id', '=', self.id)]) if self.location_id == move_in.location_id and self.location_dest_id == move_in.location_dest_id: if move_out.origin == move_in.origin: if move_out.picking_id.state != 'done': @@ -516,7 +588,7 @@ class StockPicking(models.Model): [('barcode', 'ilike', 'VL-SPOC')]).id), ('origin', '=', self.origin)]) production = self.env['mrp.production'].search([('name', '=', self.origin)]) - if move_in: + if move_in.state != 'done': move_in.write({'state': 'assigned'}) self.env['stock.move.line'].create(move_in.get_move_line(production, None)) @@ -526,7 +598,7 @@ class StockPicking(models.Model): def create_outcontract_picking(self, sorted_workorders_arr, item): m = 0 for sorted_workorders in sorted_workorders_arr: - pick_ids = [] + # pick_ids = [] if m == 0: outcontract_stock_move = self.env['stock.move'].search( [('workorder_id', '=', sorted_workorders.id), ('production_id', '=', item.id)]) @@ -545,7 +617,7 @@ class StockPicking(models.Model): outcontract_picking_type_out)) picking_out = self.create( moves_out._get_new_picking_values_Res(item, sorted_workorders, 'WH/OCOUT/')) - pick_ids.append(picking_out.id) + # pick_ids.append(picking_out.id) moves_out.write( {'picking_id': picking_out.id, 'state': 'waiting', 'workorder_id': sorted_workorders.id}) moves_out._assign_picking_post_process(new=new_picking) @@ -554,12 +626,12 @@ class StockPicking(models.Model): outcontract_picking_type_in)) picking_in = self.create( moves_in._get_new_picking_values_Res(item, sorted_workorders, 'WH/OCIN/')) - pick_ids.append(picking_in.id) + # pick_ids.append(picking_in.id) moves_in.write( {'picking_id': picking_in.id, 'state': 'waiting', 'workorder_id': sorted_workorders.id}) moves_in._assign_picking_post_process(new=new_picking) m += 1 - sorted_workorders.write({'picking_ids': [(6, 0, pick_ids)]}) + # sorted_workorders.write({'picking_ids': [(6, 0, pick_ids)]}) class ReStockMove(models.Model): @@ -590,6 +662,7 @@ class ReStockMove(models.Model): return { 'name': self.env['stock.picking']._get_name_Res(rescode), 'origin': item.name, + 'surface_technics_parameters_id': sorted_workorders.surface_technics_parameters_id.id, 'company_id': self.mapped('company_id').id, 'user_id': False, 'move_type': self.mapped('group_id').move_type or 'direct', diff --git a/sf_manufacturing/security/ir.model.access.csv b/sf_manufacturing/security/ir.model.access.csv index 9b5b5e28..fd3b0d21 100644 --- a/sf_manufacturing/security/ir.model.access.csv +++ b/sf_manufacturing/security/ir.model.access.csv @@ -150,5 +150,12 @@ access_sf_processing_panel_group_sf_order_user,sf_processing_panel_group_sf_orde access_sf_production_wizard_group_sf_order_user,sf_production_wizard_group_sf_order_user,model_sf_production_wizard,sf_base.group_sf_order_user,1,1,1,0 access_sf_processing_panel_group_plan_dispatch,sf_processing_panel_group_plan_dispatch,model_sf_processing_panel,sf_base.group_plan_dispatch,1,1,1,0 +access_sf_agv_scheduling_admin,sf_agv_scheduling_admin,model_sf_agv_scheduling,base.group_system,1,1,1,1 +access_sf_agv_scheduling_group_sf_order_user,sf_agv_scheduling_group_sf_order_user,model_sf_agv_scheduling,sf_base.group_sf_order_user,1,1,1,0 +access_sf_agv_scheduling_group_sf_mrp_manager,sf_agv_scheduling_group_sf_mrp_manager,model_sf_agv_scheduling,sf_base.group_sf_mrp_manager,1,1,1,0 +access_sf_agv_scheduling_group_sf_equipment_user,sf_agv_scheduling_group_sf_equipment_user,model_sf_agv_scheduling,sf_base.group_sf_equipment_user,1,1,1,0 + + + diff --git a/sf_manufacturing/static/src/js/customRFID.js b/sf_manufacturing/static/src/js/customRFID.js index 8f775350..cb58b3ea 100644 --- a/sf_manufacturing/static/src/js/customRFID.js +++ b/sf_manufacturing/static/src/js/customRFID.js @@ -1,16 +1,36 @@ var RFID = '' $(document).off('keydown') -console.log(2222) -$(document).on('keydown', '.modal.d-block.o_technical_modal,body.o_web_client', function (e) { - const dom = $('.customRFID') - if(!dom.length) return +$(document).on('keydown', 'body.o_web_client', function (e) { setTimeout(() => { RFID = '' + }, 200) if(e.key == 'Enter' && e.keyCode == 13 || e.key == 'Tab' && e.keyCode == 9){ - if(!RFID || RFID.length <= 3) return; - dom.children('span').text(RFID) - RFID = '' + + let fieldValue1 = $('[name="routing_type"]'); + console.log('字段值:', fieldValue1.text()); + console.log(RFID) + let fieldValue2 = $('[name="rfid_code"]'); + console.log('字段值2:', fieldValue2.text()); + // if(!RFID || RFID.length <= 3) return; + // $('[name="button_start"]').trigger('click') + // setTimeout(() => { + // $('.o_dialog .modal-footer .btn-primary').trigger('click') + // }, 50) + // RFID = '' + // return; + + // fieldValue2.val() === '') + // 检查字段值是否等于“装夹预调” + if (fieldValue1.text() === '装夹预调') { + if (!RFID || RFID.length <= 3) return; + $('[name="button_start"]').trigger('click'); + setTimeout(() => { + $('.o_dialog .modal-footer .btn-primary').trigger('click'); + }, 100); + } + + RFID = ''; return; } RFID += e.key diff --git a/sf_manufacturing/static/src/js/workpiece_delivery_wizard_confirm.js b/sf_manufacturing/static/src/js/workpiece_delivery_wizard_confirm.js new file mode 100644 index 00000000..236836ab --- /dev/null +++ b/sf_manufacturing/static/src/js/workpiece_delivery_wizard_confirm.js @@ -0,0 +1,49 @@ +odoo.define('sf_manufacturing.action_dispatch_confirm', function (require) { + const core = require('web.core'); + const ajax = require('web.ajax'); + const Dialog = require('web.Dialog'); + var rpc = require('web.rpc'); + var _t = core._t; + + async function dispatch_confirm(parent, {params}) { + const dialog = new Dialog(parent, { + title: "确认", + $content: $('
').append("请确认是否仅配送" + params.workorder_count + "个工件?"), + buttons: [ + { text: "确认", classes: 'btn-primary', close: true, click: () => dispatchConfirmed(parent, params) }, + { text: "取消", close: true }, + ], + }); + dialog.open(); + + + async function dispatchConfirmed(parent, params) { + console.log(parent, 'parent') + rpc.query({ + model: 'sf.workpiece.delivery.wizard', + method: 'confirm', + args: [params.active_id] + , + kwargs: { + context: params.context, + } + }).then(res => { + parent.services.action.doAction({ + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'target': 'new', + 'params': { + 'message': '任务下发成功!AGV任务调度编号为【' + res.name + '】', + 'type': 'success', + 'sticky': false, + 'next': {'type': 'ir.actions.act_window_close'}, + } + }); + }) + + } + } + + core.action_registry.add('dispatch_confirm', dispatch_confirm); + return dispatch_confirm; +}); diff --git a/sf_manufacturing/static/src/xml/button_show_on_tree.xml b/sf_manufacturing/static/src/xml/button_show_on_tree.xml new file mode 100644 index 00000000..00533841 --- /dev/null +++ b/sf_manufacturing/static/src/xml/button_show_on_tree.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sf_manufacturing/views/agv_scheduling_views.xml b/sf_manufacturing/views/agv_scheduling_views.xml new file mode 100644 index 00000000..c24cdf94 --- /dev/null +++ b/sf_manufacturing/views/agv_scheduling_views.xml @@ -0,0 +1,79 @@ + + + + + + agv调度 + sf.agv.scheduling + + + + + + + + + + + + + + + + + +