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 8f5ffae7..600cf1c8 100644 --- a/jikimo_frontend/static/src/js/custom_form_status_indicator.js +++ b/jikimo_frontend/static/src/js/custom_form_status_indicator.js @@ -119,13 +119,24 @@ patch(ListRenderer.prototype, 'jikimo_frontend.ListRenderer', { this.listherHeaderBodyNum() }) const treeModifiers = this.getFieldModifiers(this.props.archInfo.__rawArch); + if(treeModifiers) { - this.props.merge_key = treeModifiers.merge_key; - this.props.merge_fields = treeModifiers.merge_fields.split(','); - const data = this.setColumns(this.props.merge_key); - owl.onMounted(() => { - this.mergeColumns(this.props.merge_fields, data) - }) + if(treeModifiers.merge_fields) { + this.props.merge_key = treeModifiers.merge_key; + this.props.merge_fields = treeModifiers.merge_fields.split(','); + const data = this.setColumns(this.props.merge_key); + owl.onMounted(() => { + this.mergeColumns(this.props.merge_fields, data) + }) + } + if(treeModifiers.pacthResize) { + + owl.onPatched(() => { + this.columnWidths = null; + this.freezeColumnWidths(); + + }) + } } return this._super(...arguments); }, diff --git a/jikimo_purchase_request/__manifest__.py b/jikimo_purchase_request/__manifest__.py index 2c8bb641..2ae6ce67 100644 --- a/jikimo_purchase_request/__manifest__.py +++ b/jikimo_purchase_request/__manifest__.py @@ -8,18 +8,21 @@ 'category': 'purchase', 'depends': ['sf_manufacturing', 'purchase_request'], 'data': [ + 'security/ir.model.access.csv', 'views/sale_order_view.xml', 'views/mrp_production.xml', 'views/purchase_request_view.xml', 'wizard/purchase_request_line_make_purchase_order_view.xml', 'views/purchase_request_line_view.xml', 'views/stock_picking_views.xml', + 'wizard/purchase_request_wizard_views.xml', + 'views/purchase_request_menu_views.xml', ], 'assets': { - 'web.assets_backend': [ - 'jikimo_purchase_request/static/src/**/*' - ], - }, + 'web.assets_backend': [ + 'jikimo_purchase_request/static/src/**/*' + ], + }, 'application': True, 'installable': True, 'auto_install': False, diff --git a/jikimo_purchase_request/i18n/zh_CN.po b/jikimo_purchase_request/i18n/zh_CN.po index 5a43c3a2..c6a51f5c 100644 --- a/jikimo_purchase_request/i18n/zh_CN.po +++ b/jikimo_purchase_request/i18n/zh_CN.po @@ -410,7 +410,7 @@ msgstr "显示名称" #: model_terms:ir.ui.view,arch_db:purchase_request.view_purchase_request_form #: model_terms:ir.ui.view,arch_db:purchase_request.view_purchase_request_search msgid "Done" -msgstr "完成" +msgstr "关闭" #. module: purchase_request #: model:ir.model.fields,field_description:purchase_request.field_purchase_request_line__move_dest_ids diff --git a/jikimo_purchase_request/models/mrp_production.py b/jikimo_purchase_request/models/mrp_production.py index 58a94eba..6250c79e 100644 --- a/jikimo_purchase_request/models/mrp_production.py +++ b/jikimo_purchase_request/models/mrp_production.py @@ -18,9 +18,8 @@ class MrpProduction(models.Model): # pr_ids = self.env['purchase.request'].sudo().search([('origin', 'like', item.name)]) # item.pr_mp_count = len(pr_ids) # 由于采购申请合并了所有销售订单行的采购,所以不区分产品 - first_mp = self.env['mrp.production'].search( - [('origin', '=', item.origin)], limit=1, order='id asc') - pr_ids = self.env['purchase.request'].sudo().search([('origin', 'like', first_mp.name)]) + mrp_names = self.env['mrp.production'].search([('origin', '=', item.origin)]).mapped('name') + pr_ids = self.env['purchase.request'].sudo().search([('origin', 'in', mrp_names)]) item.pr_mp_count = len(pr_ids) # pr_ids = self.env['purchase.request'].sudo().search([('origin', 'like', item.name), ('is_subcontract', '!=', 'True')]) @@ -37,9 +36,8 @@ class MrpProduction(models.Model): # else: # pr_ids = self.env['purchase.request'].sudo().search([('origin', 'like', self.name)]) # 由于采购申请合并了所有销售订单行的采购,所以不区分产品 - first_mp = self.env['mrp.production'].search( - [('origin', '=', self.origin)], limit=1, order='id asc') - pr_ids = self.env['purchase.request'].sudo().search([('origin', 'like', first_mp.name)]) + mrp_names = self.env['mrp.production'].search([('origin', '=', self.origin)]).mapped('name') + pr_ids = self.env['purchase.request'].sudo().search([('origin', 'in', mrp_names)]) action = { diff --git a/jikimo_purchase_request/models/purchase_order.py b/jikimo_purchase_request/models/purchase_order.py index f22027fb..95f81e9f 100644 --- a/jikimo_purchase_request/models/purchase_order.py +++ b/jikimo_purchase_request/models/purchase_order.py @@ -1,4 +1,5 @@ from odoo import api, fields, models, _ +from odoo.tools import float_compare class PurchaseOrder(models.Model): @@ -13,4 +14,43 @@ class PurchaseOrder(models.Model): ('done', '完成'), ('cancel', '取消'), ('rejected', '已驳回') - ], string='Status', readonly=True, index=True, copy=False, default='draft', tracking=True) \ No newline at end of file + ], string='Status', readonly=True, index=True, copy=False, default='draft', tracking=True) + + + def button_confirm(self): + res = super(PurchaseOrder, self).button_confirm() + # 取消反向调拨单 + reverse_move_ids = self.env['stock.move'].search([ + ('origin', '=', self.name), + ('purchase_line_id', '=', False), + ('state', '!=', 'done') + ]) + if reverse_move_ids: + reverse_move_ids.picking_id.action_cancel() + return res + + def button_cancel(self): + """ + 将取消的采购订单关联的库存移动撤销 + """ + move_ids = self.order_line.move_dest_ids.filtered(lambda move: move.state != 'done' and not move.scrapped) + res =super(PurchaseOrder, self).button_cancel() + if move_ids.mapped('created_purchase_request_line_id'): + move_ids.write({'state': 'waiting', 'is_done': False}) + return res + + def write(self, vals): + res = super(PurchaseOrder, self).write(vals) + if 'state' in vals and vals['state'] == 'purchase': + purchase_request = self.order_line.purchase_request_lines.request_id + if purchase_request: + finished = True + # 判断该采购申请所有明细行是否都完成 + for purchase_request_line in purchase_request.line_ids: + finished_qty = sum(purchase_request_line.purchase_lines.filtered(lambda line: line.state == 'purchase').mapped('product_qty')) + if float_compare(finished_qty ,purchase_request_line.product_qty, precision_rounding=purchase_request_line.product_id.uom_id.rounding) < 0: + finished = False + break + if finished: + purchase_request.button_done() + return res diff --git a/jikimo_purchase_request/models/purchase_request.py b/jikimo_purchase_request/models/purchase_request.py index 3ecd2161..0459dcd9 100644 --- a/jikimo_purchase_request/models/purchase_request.py +++ b/jikimo_purchase_request/models/purchase_request.py @@ -1,13 +1,15 @@ import re import ast -from odoo import models, fields, api +from odoo import models, fields, api, _ +from itertools import groupby +from odoo.tools import float_compare class PurchaseRequest(models.Model): _inherit = 'purchase.request' _description = '采购申请' - # 为state添加取消状态 + # 为state添加取消状态 state = fields.Selection( selection_add=[('cancel', '已取消')], ondelete={'cancel': 'set default'} # 添加 ondelete 策略 @@ -29,6 +31,56 @@ class PurchaseRequest(models.Model): action['context'] = origin_context return action + def button_done(self): + product_qty_map = {key: sum(line.product_qty for line in group) for key, group in + groupby(self.line_ids, key=lambda x: x.product_id.id)} + lines = self.mapped("line_ids.purchase_lines.order_id") + # 采购单产品和数量 + product_summary = {} + product_rounding = {} + if lines: + for line in lines: + for line_item in line.order_line: + product_id = line_item.product_id.id + qty = line_item.product_qty + product_rounding[product_id] = line_item.product_id.uom_id.rounding + if product_id in product_summary: + product_summary[product_id] += qty + else: + product_summary[product_id] = qty + + # 校验产品数量 + discrepancies = [] + for product_id, qty in product_qty_map.items(): + if product_id in product_summary: + if float_compare(product_summary[product_id], qty, precision_rounding=product_rounding[product_id]) < 0: + discrepancies.append((product_id, qty, product_summary[product_id])) + else: + discrepancies.append((product_id, qty, 0)) + + if discrepancies: + # 弹出提示框 + message = "产品数量不一致:\n" + for product_id, required_qty, order_qty in discrepancies: + product_name = self.env['product.product'].browse(product_id).display_name # 获取产品名称 + message += f"产品 {product_name},需求数量 {required_qty},关联采购订单数量 {order_qty}(含询价状态)\n" + # 添加确认框 + message += "确认关闭?" + return { + 'name': _('采购申请'), + 'type': 'ir.actions.act_window', + 'views': [(self.env.ref( + 'jikimo_purchase_request.purchase_request_wizard_wizard_form_view').id, + 'form')], + 'res_model': 'purchase.request.wizard', + 'target': 'new', + 'context': { + 'default_purchase_request_id': self.id, + 'default_message': message, + }} + return super(PurchaseRequest, self).button_done() + + class PurchaseRequestLine(models.Model): _inherit = 'purchase.request.line' _description = '采购申请明细' @@ -47,7 +99,8 @@ class PurchaseRequestLine(models.Model): ('outsourcing', "委外加工"), ], string='供货方式', compute='_compute_supply_method', store=True) - purchase_request_count = fields.Integer(string='采购申请数量', compute='_compute_purchase_request_count', readonly=True) + purchase_request_count = fields.Integer(string='采购申请数量', compute='_compute_purchase_request_count', + readonly=True) purchase_count = fields.Integer(string="采购订单数量", compute="_compute_purchase_count", readonly=True) @api.depends("purchase_lines") @@ -92,7 +145,7 @@ class PurchaseRequestLine(models.Model): continue if record.product_id.categ_id.name == '坯料': product_name = '' - match = re.search(r'(S\d{5}-\d)', record.product_id.name) + match = re.search(r'(S\d{5}-\d+)', record.product_id.name) # 如果匹配成功,提取结果 if match: product_name = match.group(0) @@ -114,7 +167,10 @@ class PurchaseRequestLine(models.Model): def _compute_qty_to_buy(self): for pr in self: - qty_to_buy = sum(pr.mapped("product_qty")) - sum(pr.mapped("qty_done")) - sum(pr.mapped("qty_in_progress")) + qty_to_buy = sum(pr.mapped("product_qty")) + if pr.purchase_count > 0: + qty_to_buy -= sum(pr.mapped("purchase_lines").filtered(lambda po: po.state != 'cancel').mapped( + "product_qty")) pr.qty_to_buy = qty_to_buy > 0.0 pr.pending_qty_to_receive = qty_to_buy diff --git a/jikimo_purchase_request/models/stock_picking.py b/jikimo_purchase_request/models/stock_picking.py index d8f15f6b..abac1b1b 100644 --- a/jikimo_purchase_request/models/stock_picking.py +++ b/jikimo_purchase_request/models/stock_picking.py @@ -33,3 +33,15 @@ class StockPicking(models.Model): 'view_mode': 'tree,form', }) return action + + def _action_done(self): + res = super(StockPicking, self)._action_done() + # 将新产生的backorder对应上原来的采购申请明细行 + backorder_ids = self.backorder_ids + if backorder_ids: + purchase_request_lines = self.move_ids.move_orig_ids.purchase_line_id.purchase_request_lines + if purchase_request_lines: + purchase_request_lines.move_dest_ids = [ + (4, x.id) for x in backorder_ids.move_ids if x.product_id.id == purchase_request_lines.product_id.id + ] + return res \ No newline at end of file diff --git a/jikimo_purchase_request/models/stock_rule.py b/jikimo_purchase_request/models/stock_rule.py index 02dd3626..4a99e804 100644 --- a/jikimo_purchase_request/models/stock_rule.py +++ b/jikimo_purchase_request/models/stock_rule.py @@ -79,12 +79,4 @@ class StockRule(models.Model): ) res = super(StockRule, self)._run_buy(new_procurements) - # 判断是否根据规则生成新的采购申请单据,如果生成则修改状态为 approved - origins = list(set([procurement[0].origin for procurement in procurements])) - for origin in origins: - pr_ids = self.env["purchase.request"].sudo().search( - [('origin', 'like', origin), ('rule_new_add', '=', True), ('state', '=', 'draft')]) - if pr_ids: - pr_ids.write({'need_validation': False}) - pr_ids.write({"state": "approved", 'need_validation': True, 'rule_new_add': False}) return res diff --git a/jikimo_purchase_request/security/ir.model.access.csv b/jikimo_purchase_request/security/ir.model.access.csv new file mode 100644 index 00000000..7258fb23 --- /dev/null +++ b/jikimo_purchase_request/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_purchase_request_wizard_group_user,purchase.request.wizard,model_purchase_request_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/jikimo_purchase_request/views/purchase_request_menu_views.xml b/jikimo_purchase_request/views/purchase_request_menu_views.xml new file mode 100644 index 00000000..581706c8 --- /dev/null +++ b/jikimo_purchase_request/views/purchase_request_menu_views.xml @@ -0,0 +1,21 @@ + + + + + 采购申请 + + 2 + + + + 1 + + + + + 10 + + + + + diff --git a/jikimo_purchase_request/views/purchase_request_view.xml b/jikimo_purchase_request/views/purchase_request_view.xml index 9976b52a..151152e8 100644 --- a/jikimo_purchase_request/views/purchase_request_view.xml +++ b/jikimo_purchase_request/views/purchase_request_view.xml @@ -15,6 +15,26 @@ + + + + + 1 + + + oe_highlight + + + + + + purchase.request.sf.tree + purchase.request + + + + hide + @@ -63,4 +83,9 @@ + + + Purchase Requests + + \ No newline at end of file diff --git a/jikimo_purchase_request/wizard/__init__.py b/jikimo_purchase_request/wizard/__init__.py index 0a2ad149..f19f8340 100644 --- a/jikimo_purchase_request/wizard/__init__.py +++ b/jikimo_purchase_request/wizard/__init__.py @@ -1,3 +1,4 @@ # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) from . import purchase_request_line_make_purchase_order +from . import purchase_request_wizard diff --git a/jikimo_purchase_request/wizard/purchase_request_wizard.py b/jikimo_purchase_request/wizard/purchase_request_wizard.py new file mode 100644 index 00000000..94bf327a --- /dev/null +++ b/jikimo_purchase_request/wizard/purchase_request_wizard.py @@ -0,0 +1,12 @@ +from odoo import models, fields, api + + +class PurchaseRequestWizard(models.TransientModel): + _name = 'purchase.request.wizard' + _description = '采购申请向导' + + purchase_request_id = fields.Many2one('purchase.request', string='采购申请') + message = fields.Char(string='提示', readonly=True) + + def confirm(self): + return self.purchase_request_id.write({"state": "done"}) diff --git a/jikimo_purchase_request/wizard/purchase_request_wizard_views.xml b/jikimo_purchase_request/wizard/purchase_request_wizard_views.xml new file mode 100644 index 00000000..6430a314 --- /dev/null +++ b/jikimo_purchase_request/wizard/purchase_request_wizard_views.xml @@ -0,0 +1,22 @@ + + + + purchase.request.wizard.form.view + purchase.request.wizard + +
+ +
+
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/jikimo_purchase_request_tier_validation/__manifest__.py b/jikimo_purchase_request_tier_validation/__manifest__.py index 403297ab..0665e035 100644 --- a/jikimo_purchase_request_tier_validation/__manifest__.py +++ b/jikimo_purchase_request_tier_validation/__manifest__.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- { - 'name': "机企猫 采购审批流程", + 'name': "机企猫 采购申请审批流程", 'summary': """ - Short (1 phrase/line) summary of the module's purpose, used as - subtitle on modules listing or apps.openerp.com""", + 采购申请审批流程""", 'description': """ Long description of module's purpose diff --git a/jikimo_purchase_request_tier_validation/models/__init__.py b/jikimo_purchase_request_tier_validation/models/__init__.py index 5305644d..ff8a54e8 100644 --- a/jikimo_purchase_request_tier_validation/models/__init__.py +++ b/jikimo_purchase_request_tier_validation/models/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- -from . import models \ No newline at end of file +from . import models +from . import stock_rule \ No newline at end of file diff --git a/jikimo_purchase_request_tier_validation/models/models.py b/jikimo_purchase_request_tier_validation/models/models.py index 0070a4c7..3a8cb027 100644 --- a/jikimo_purchase_request_tier_validation/models/models.py +++ b/jikimo_purchase_request_tier_validation/models/models.py @@ -22,3 +22,9 @@ class PurchaseRequest(models.Model): self.state = 'approved' return res + + @api.model + def _get_under_validation_exceptions(self): + res = super(PurchaseRequest, self)._get_under_validation_exceptions() + res.append("state") + return res diff --git a/jikimo_purchase_request_tier_validation/models/stock_rule.py b/jikimo_purchase_request_tier_validation/models/stock_rule.py new file mode 100644 index 00000000..6bc521f2 --- /dev/null +++ b/jikimo_purchase_request_tier_validation/models/stock_rule.py @@ -0,0 +1,16 @@ +from odoo import models, api + +class StockRule(models.Model): + _inherit = 'stock.rule' + + def _run_buy(self, procurements): + res = super(StockRule, self)._run_buy(procurements) + # 判断是否根据规则生成新的采购申请单据,如果生成则修改状态为 approved + origins = list(set([procurement[0].origin for procurement in procurements])) + for origin in origins: + pr_ids = self.env["purchase.request"].sudo().search( + [('origin', 'like', origin), ('rule_new_add', '=', True), ('state', '=', 'draft')]) + if pr_ids: + pr_ids.write({'need_validation': False}) + pr_ids.write({"state": "approved", 'need_validation': True, 'rule_new_add': False}) + return res diff --git a/jikimo_purchase_tier_validation/__manifest__.py b/jikimo_purchase_tier_validation/__manifest__.py index ed0cecb7..c1c2c075 100644 --- a/jikimo_purchase_tier_validation/__manifest__.py +++ b/jikimo_purchase_tier_validation/__manifest__.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- { - 'name': "机企猫 采购申请审批流程", + 'name': "机企猫 采购审批流程", 'summary': """ - 采购申请审批流程""", + 采购审批流程""", 'description': """ - 采购申请审批流程""", + 采购审批流程""", 'author': "My Company", 'website': "https://www.yourcompany.com", diff --git a/jikimo_workorder_exception/controllers/main.py b/jikimo_workorder_exception/controllers/main.py index cf208700..6134e27f 100644 --- a/jikimo_workorder_exception/controllers/main.py +++ b/jikimo_workorder_exception/controllers/main.py @@ -4,6 +4,7 @@ import json import logging from odoo.addons.sf_mrs_connect.controllers.controllers import Sf_Mrs_Connect from odoo.addons.sf_manufacturing.controllers.controllers import Manufacturing_Connect +from odoo.addons.sf_base.decorators.api_log import api_log from datetime import datetime _logger = logging.getLogger(__name__) @@ -12,6 +13,7 @@ class WorkorderExceptionConroller(http.Controller): @http.route('/AutoDeviceApi/BillError', type='json', auth='public', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('工单对接错误', requester='中控系统') def workder_exception(self, **kw): """ 记录工单异常 diff --git a/quality_control/models/quality.py b/quality_control/models/quality.py index dbf4f0b4..22eef194 100644 --- a/quality_control/models/quality.py +++ b/quality_control/models/quality.py @@ -133,6 +133,7 @@ class QualityCheck(models.Model): part_name = fields.Char('零件名称', related='product_id.part_name', readonly=False, store=True) part_number = fields.Char('零件图号', related='product_id.part_number', readonly=False, store=True) material_name = fields.Char('材料名称', compute='_compute_material_name') + model_id = fields.Char('模型ID', related='product_id.model_id') # # 总数量,值为调拨单_产品明细_数量 # total_qty = fields.Float('总数量', compute='_compute_total_qty', readonly=True) @@ -338,7 +339,7 @@ class QualityCheck(models.Model): # 4. 获取报告动作并生成PDF(此时二维码将包含正确的文档ID) report_action = self.env.ref('sf_quality.action_report_quality_inspection') - pdf_content, _ = report_action._render_qweb_pdf( + pdf_content, v = report_action._render_qweb_pdf( report_ref=report_action.report_name, res_ids=self.ids ) diff --git a/quality_control/views/quality_views.xml b/quality_control/views/quality_views.xml index beed759e..9e1aa961 100644 --- a/quality_control/views/quality_views.xml +++ b/quality_control/views/quality_views.xml @@ -493,6 +493,9 @@ + + + diff --git a/sf_base/commons/common.py b/sf_base/commons/common.py index 2dbb4795..38b3d49a 100644 --- a/sf_base/commons/common.py +++ b/sf_base/commons/common.py @@ -103,12 +103,19 @@ class PrintingUtils(models.AbstractModel): self.send_to_printer(host, port, zpl_code) - def add_qr_code_to_pdf(self, pdf_path:str, content:str, buttom_text:Optional[str]=False): + def add_qr_code_to_pdf( + self, + pdf_path:str, + content:str, + qr_code_buttom_text:Optional[str]=False, + buttom_text:Optional[str]=False, + ): """ 在PDF文件中添加二维码 :param pdf_path: PDF文件路径 :param content: 二维码内容 - :param buttom_text: 二维码下方文字 + :param qr_code_buttom_text: 二维码下方文字 + :param buttom_text: 正文下方文字 :return: 是否成功 """ if not os.path.exists(pdf_path): @@ -156,8 +163,9 @@ class PrintingUtils(models.AbstractModel): existing_pdf = PdfFileReader(original_file) output = PdfFileWriter() - # 处理第一页 - page = existing_pdf.getPage(0) + # 处理最后一页 + last_page = existing_pdf.getNumPages() - 1 + page = existing_pdf.getPage(last_page) # 获取页面尺寸 page_width = float(page.mediaBox.getWidth()) page_height = float(page.mediaBox.getHeight()) @@ -179,13 +187,29 @@ class PrintingUtils(models.AbstractModel): qr_y = margin + 20 # 将二维码向上移动一点,为文字留出空间 c.drawImage(qr_temp_path, page_width - qr_size - margin, qr_y, width=qr_size, height=qr_size) - if buttom_text: + if qr_code_buttom_text: # 在二维码下方绘制文字 - text = buttom_text + text = qr_code_buttom_text text_width = c.stringWidth(text, "SimSun" if font_found else "Helvetica", 10) # 准确计算文字宽度 text_x = page_width - qr_size - margin + (qr_size - text_width) / 2 # 文字居中对齐 text_y = margin + 20 # 文字位置靠近底部 c.drawString(text_x, text_y, text) + + # 设置字体 + if font_found: + c.setFont('SimSun', 12) # 增大字体大小到14pt + else: + # 如果没有找到中文字体,使用默认字体 + c.setFont('Helvetica', 120) + logging.warning("未找到中文字体,将使用默认字体") + + if buttom_text: + # 在下方中间添加文字 + text = buttom_text + text_width = c.stringWidth(text, "SimSun" if font_found else "Helvetica", 12) # 准确计算文字宽度 + text_x = (page_width - text_width) / 2 # 文字居中对齐 + text_y = margin + 20 # 文字位置靠近底部 + c.drawString(text_x, text_y, text) c.save() @@ -196,11 +220,12 @@ class PrintingUtils(models.AbstractModel): # 合并原始页面和二维码页面 page.mergePage(qr_page) - output.addPage(page) # 添加剩余的页面 - for i in range(1, existing_pdf.getNumPages()): + for i in range(0, last_page): output.addPage(existing_pdf.getPage(i)) + + output.addPage(page) # 保存最终的PDF到一个临时文件 final_temp_path = pdf_path + '.tmp' diff --git a/sf_base/controllers/controllers.py b/sf_base/controllers/controllers.py index c8447d4e..8694170e 100644 --- a/sf_base/controllers/controllers.py +++ b/sf_base/controllers/controllers.py @@ -4,6 +4,7 @@ import json import logging from odoo import http from odoo.http import request +from odoo.addons.sf_base.decorators.api_log import api_log _logger = logging.getLogger(__name__) @@ -11,6 +12,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/MachineToolGroup', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('机床刀具组', requester='中控系统') def get_maintenance_tool_groups_Info(self, **kw): """ 机床刀具组接口 diff --git a/sf_base/decorators/api_log.py b/sf_base/decorators/api_log.py index 2838ec55..634e0aed 100644 --- a/sf_base/decorators/api_log.py +++ b/sf_base/decorators/api_log.py @@ -27,21 +27,27 @@ def api_log(name=None, requester=None): # 执行原始函数 result = func(*args, **kwargs) + origin_result = result + if isinstance(result, str): + result = json.loads(result) # 计算响应时间 end_time = datetime.now() response_time = (end_time - start_time).total_seconds() + + # 获取响应状态 + status = result.get('code') if 'code' in result else result.get('ErrorCode') if 'ErrorCode' in result else 200 # 创建日志记录 log_vals = { 'name': name or func.__name__, 'path': path, - 'method': method, + 'method': method.upper(), 'request_data': json.dumps(request_data, ensure_ascii=False), 'response_data': json.dumps(result, ensure_ascii=False), 'remote_addr': remote_addr, 'response_time': response_time, - 'status': result.get('code', 500), + 'status': 200 if status == 0 else status, 'requester': requester, 'responser': '智能工厂' } @@ -49,7 +55,7 @@ def api_log(name=None, requester=None): # 异步创建日志记录 request.env['api.request.log'].sudo().with_context(tracking_disable=True).create(log_vals) - return result + return origin_result except Exception as e: _logger.error(f"API日志记录失败: {str(e)}") diff --git a/sf_base/models/api_log.py b/sf_base/models/api_log.py index 4b630b88..6fbecf3d 100644 --- a/sf_base/models/api_log.py +++ b/sf_base/models/api_log.py @@ -1,4 +1,9 @@ from odoo import models, fields, api +import json, ast +import logging +import requests + +_logger = logging.getLogger(__name__) class ApiRequestLog(models.Model): @@ -16,3 +21,52 @@ class ApiRequestLog(models.Model): status = fields.Integer('状态码') requester = fields.Char('请求方') responser = fields.Char('响应方') + + @api.model + def log_request(self, method, url, name=None, responser=None, **kwargs): + # Log the request + request_headers = kwargs.get('headers', {}) + request_body = kwargs.get('json') or kwargs.get('params') or {} + + _logger.info(f"Request: {method} {url} Headers: {request_headers} Body: {request_body}") + + # Make the actual request + response = requests.request(method, url, **kwargs) + + # Log the response + response_status = response.status_code + response_headers = response.headers + response_body = response.text + response_time = response.elapsed.total_seconds() + + _logger.info(f"Response: Status: {response_status} Headers: {response_headers} Body: {response_body}") + + try: + # 如果是字符串,先尝试用 ast.literal_eval 安全地转换成 Python 对象 + if isinstance(response_body, str): + + response_body_obj = json.loads(response_body) + else: + response_body_obj = response_body + + # 再使用 json.dumps 转换成标准的 JSON 字符串 + response_body = json.dumps(response_body_obj, ensure_ascii=False) + except Exception as e: + _logger.warning(f"转换 response_body 到标准 JSON 失败: {str(e)}") + # 如果转换失败,保持原样 + + # Save to database + self.sudo().create({ + 'name': name, + 'path': url, + 'method': method.upper(), + 'request_data': request_body, + 'response_data': response_body, + 'remote_addr': None, + 'response_time': response_time, + 'status': response_status, + 'requester': '智能工厂', + 'responser': responser + }) + + return response \ No newline at end of file diff --git a/sf_base/views/api_log_views.xml b/sf_base/views/api_log_views.xml index 05389fc7..f9c9b6ce 100644 --- a/sf_base/views/api_log_views.xml +++ b/sf_base/views/api_log_views.xml @@ -5,13 +5,15 @@ api.request.log - - - + + + + + @@ -32,6 +34,8 @@ + + @@ -48,6 +52,23 @@ + + api.request.log.search + api.request.log + + + + + + + + + + + + + + API请求日志 api.request.log diff --git a/sf_dlm/models/product_supplierinfo.py b/sf_dlm/models/product_supplierinfo.py index a46f4856..f4278e03 100644 --- a/sf_dlm/models/product_supplierinfo.py +++ b/sf_dlm/models/product_supplierinfo.py @@ -17,7 +17,7 @@ class ResProductCategory(models.Model): class ResProductProduct(models.Model): _inherit = 'product.product' - single_manufacturing = fields.Boolean(string="单个制造") + # single_manufacturing = fields.Boolean(string="单个制造") is_bfm = fields.Boolean('业务平台是否自动创建', default=False) diff --git a/sf_dlm_management/models/mrp_routing_workcenter.py b/sf_dlm_management/models/mrp_routing_workcenter.py index dd9e59e3..f104cd13 100644 --- a/sf_dlm_management/models/mrp_routing_workcenter.py +++ b/sf_dlm_management/models/mrp_routing_workcenter.py @@ -2,8 +2,8 @@ # from odoo import fields, models, api # from odoo.exceptions import UserError # from odoo.tools import str2bool -# -# + + # class ResMrpRoutingWorkcenter(models.Model): # _inherit = 'mrp.routing.workcenter' # def init(self): diff --git a/sf_dlm_management/models/sf_production_common.py b/sf_dlm_management/models/sf_production_common.py index 42cb9ff2..f3141892 100644 --- a/sf_dlm_management/models/sf_production_common.py +++ b/sf_dlm_management/models/sf_production_common.py @@ -3,12 +3,12 @@ # from odoo import fields, models, api # from odoo.exceptions import UserError # from odoo.tools import str2bool -# -# + + # class SfProductionProcessParameter(models.Model): # _inherit = 'sf.production.process.parameter' -# -# + + # @api.model # def create(self, vals): # # if vals.get('code', '/') == '/' or vals.get('code', '/') is False: @@ -26,7 +26,7 @@ # def create_service_product(self): # service_categ = self.env.ref( # 'sf_dlm.product_category_surface_technics_sf').sudo() -# + # product_name = f"{self.process_id.name}_{self.name}" # product_id = self.env['product.template'].search( # [("name", '=', product_name)]) @@ -48,7 +48,7 @@ # 'partner_id': res_partner.id, # 'price': 1, })], # }) -# + # def create_work_center(self): # production_process_parameter = self # if not production_process_parameter.process_id: @@ -70,7 +70,7 @@ # production_process_parameter.routing_id = routing_id.id # else: # production_process_parameter.routing_id = workcenter_id.id -# + # def init(self): # super(SfProductionProcessParameter, self).init() # # 在模块初始化时触发计算字段的更新 diff --git a/sf_manufacturing/__manifest__.py b/sf_manufacturing/__manifest__.py index 24c8a4b0..f30c1300 100644 --- a/sf_manufacturing/__manifest__.py +++ b/sf_manufacturing/__manifest__.py @@ -48,6 +48,7 @@ 'views/mrp_workorder_batch_replan.xml', 'views/purchase_order_view.xml', 'views/product_template_views.xml', + # 'views/stock_warehouse_orderpoint.xml', ], 'assets': { diff --git a/sf_manufacturing/controllers/controllers.py b/sf_manufacturing/controllers/controllers.py index 64a981ad..70b0b87b 100644 --- a/sf_manufacturing/controllers/controllers.py +++ b/sf_manufacturing/controllers/controllers.py @@ -6,12 +6,14 @@ from datetime import datetime from odoo.addons.sf_manufacturing.models.agv_scheduling import RepeatTaskException from odoo import http from odoo.http import request +from odoo.addons.sf_base.decorators.api_log import api_log class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/GetWoInfo', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('获取工单', requester='中控系统') def get_Work_Info(self, **kw): """ 自动化传递工单号获取工单信息 @@ -54,6 +56,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/GetShiftPlan', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('获取日计划', requester='中控系统') def get_ShiftPlan(self, **kw): """ 自动化每天获取机台日计划 @@ -107,6 +110,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/QcCheck', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('工件预调(前置三元检测)', requester='中控系统') def get_qcCheck(self, **kw): """ 工件预调(前置三元检测) @@ -149,6 +153,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/FeedBackStart', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('工单开始', requester='中控系统') def button_Work_START(self, **kw): """ 工单任务开始 @@ -198,6 +203,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/FeedBackEnd', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('工单结束', requester='中控系统') def button_Work_End(self, **kw): """ 工单任务结束 @@ -249,6 +255,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/PartQualityInspect', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('零件检测(后置三元检测)', requester='中控系统') def PartQualityInspect(self, **kw): """ 零件质检 @@ -295,6 +302,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/CMMProgDolod', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('CMM测量程序下载', requester='中控系统') def CMMProgDolod(self, **kw): """ 中控系统传递RFID编号给MES,获取测量程序文件。Ftp下载文件 @@ -335,6 +343,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/NCProgDolod', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('CAM加工程序下载', requester='中控系统') def NCProgDolod(self, **kw): """ 中控系统传递RFID编号给MES,获取程序单及程序文件。Ftp下载文件 @@ -376,6 +385,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/LocationChange', type='json', auth='sf_token', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('库位变更', requester='中控系统') def LocationChange(self, **kw): """ 库位变更 @@ -480,6 +490,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/AGVToProduct', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('AGV运送上产线', requester='中控系统') def AGVToProduct(self, **kw): """ AGV运送上产线(完成) @@ -552,6 +563,7 @@ class Manufacturing_Connect(http.Controller): @http.route('/AutoDeviceApi/AGVDownProduct', type='json', auth='none', methods=['GET', 'POST'], csrf=False, cors="*") + @api_log('AGV运送下产线', requester='中控系统') def AGVDownProduct(self, **kw): """ MES调度AGV,搬运零件AGV托盘到产线接驳站。 diff --git a/sf_manufacturing/controllers/main.py b/sf_manufacturing/controllers/main.py index 3dd73017..d3e700a0 100644 --- a/sf_manufacturing/controllers/main.py +++ b/sf_manufacturing/controllers/main.py @@ -45,6 +45,9 @@ class JikimoSaleRoutePicking(Sf_Bf_Connect): product.product_tmpl_id.is_customer_provided = True if item['embryo_redundancy_id'] else False order_id.with_user(request.env.ref("base.user_admin")).sale_order_create_line(product, item) i += 1 + if kw.get('contract_file_name') and kw.get('contract_file') and kw.get('contract_code'): + order_id.create_sale_documents(kw.get('contract_file_name'), kw.get('contract_file')) + order_id.write({'contract_code': kw.get('contract_code')}) res['factory_order_no'] = order_id.name order_id.confirm_to_supply_method() except Exception as e: diff --git a/sf_manufacturing/models/__init__.py b/sf_manufacturing/models/__init__.py index bccaf634..8af783e7 100644 --- a/sf_manufacturing/models/__init__.py +++ b/sf_manufacturing/models/__init__.py @@ -18,3 +18,4 @@ from . import quick_easy_order from . import purchase_order from . import quality_check from . import purchase_request_line +# from . import stock_warehouse_orderpoint \ No newline at end of file diff --git a/sf_manufacturing/models/mrp_production.py b/sf_manufacturing/models/mrp_production.py index 25a016e3..74460d4c 100644 --- a/sf_manufacturing/models/mrp_production.py +++ b/sf_manufacturing/models/mrp_production.py @@ -279,7 +279,7 @@ class MrpProduction(models.Model): production_id.part_name = production_id.product_id.part_name elif production_id.product_id.categ_id.type == '坯料': product_name = '' - match = re.search(r'(S\d{5}-\d)', production_id.product_id.name) + match = re.search(r'(S\d{5}-\d+)', production_id.product_id.name) # 如果匹配成功,提取结果 if match: product_name = match.group(0) @@ -636,13 +636,17 @@ class MrpProduction(models.Model): # 增加触发时间参数 def update_programming_state(self, trigger_time=None, reprogramming_reason=None): try: + reason = "" manufacturing_type = None if self.is_scrap: manufacturing_type = 'scrap' + reason = "报废" elif self.tool_state == '2': manufacturing_type = 'invalid_tool_rework' + reason = "无效功能刀具" elif self.is_rework: manufacturing_type = 'rework' + reason = "返工" res = {'programming_no': self.programming_no, 'manufacturing_type': manufacturing_type, 'trigger_time': trigger_time, @@ -657,6 +661,16 @@ class MrpProduction(models.Model): result = json.loads(ret['result']) logging.info('update_programming_state-ret:%s' % result) if result['status'] == 1: + self.programming_record_ids.create({ + 'number': len(self.programming_record_ids) + 1, + 'production_id': self.id, + 'reason': reason, + 'programming_method': False, + 'current_programming_count': False, + 'target_production_id': False, + 'apply_time': fields.Datetime.now(), + 'send_time': False, + }) self.write({'is_rework': True}) else: raise UserError(ret['message']) @@ -787,6 +801,17 @@ class MrpProduction(models.Model): if ret['status'] == 1: self.write( {'programming_no': ret['programming_no'], 'programming_state': '编程中', 'work_state': '编程中'}) + # 生成编程记录 + self.programming_record_ids.create({ + 'number': len(self.programming_record_ids) + 1, + 'production_id': self.id, + 'reason': '首次下发', + 'programming_method': False, + 'current_programming_count': False, + 'target_production_id': False, + 'apply_time': fields.Datetime.now(), + 'send_time': False, + }) else: raise UserError(ret['message']) except Exception as e: @@ -928,12 +953,13 @@ class MrpProduction(models.Model): # 'sf_stock.stock_route_process_outsourcing').id)] # for product_id, request_line_list in grouped_purchase_request_line_sorted_list.items(): # cur_request_line = request_line_list[0] - # cur_request_line['product_qty'] = len(request_line_list) + # # cur_request_line['product_qty'] = cur_request_line['product_qty'] # cur_request_line['request_id'] = pr.id # cur_request_line['origin'] = ", ".join({item['production_name'] for item in request_line_list if item.get('production_name')}) # cur_request_line.pop('group_id', None) # cur_request_line.pop('production_name', None) # self.env["purchase.request.line"].create(cur_request_line) + # pr.button_approved() # 外协出入库单处理 def get_subcontract_pick_purchase(self): @@ -1771,7 +1797,6 @@ class MrpProduction(models.Model): """ 检查前置条件:制造订单【状态】=“待排程、待加工”,制造订单的【编程状态】=“已编程”。 """ - print('申请编程') if len(self) > 1: raise UserError('仅支持选择单个制造订单进行编程申请,请重新选择') for production in self: @@ -1840,6 +1865,7 @@ class sf_programming_record(models.Model): target_production_id = fields.Char('目标制造单号') apply_time = fields.Datetime('申请时间') send_time = fields.Datetime('下发时间') + apply_uid = fields.Many2one('res.users', '申请人', default=lambda self: self.env.user) class sf_detection_result(models.Model): diff --git a/sf_manufacturing/models/mrp_workorder.py b/sf_manufacturing/models/mrp_workorder.py index 2c32ed9c..9bfc09ec 100644 --- a/sf_manufacturing/models/mrp_workorder.py +++ b/sf_manufacturing/models/mrp_workorder.py @@ -71,7 +71,7 @@ class ResMrpWorkOrder(models.Model): tracking=True) back_button_display = fields.Boolean(default=False, compute='_compute_back_button_display', store=True) # pr_mp_count = fields.Integer('采购申请单数量', compute='_compute_pr_mp_count', store=True) - # + # @api.depends('state') # def _compute_pr_mp_count(self): # for item in self: @@ -85,6 +85,7 @@ class ResMrpWorkOrder(models.Model): # item.pr_mp_count = len(pr_ids) # else: # item.pr_mp_count = 0 + @api.depends('state') def _compute_back_button_display(self): for record in self: @@ -129,8 +130,14 @@ class ResMrpWorkOrder(models.Model): record.back_button_display = False else: next_workorder = sorted_workorders[position + 1] - next_state = next_workorder.state - if (next_state == 'ready' or ( + # 持续获取下一个工单,直到找到一个不是返工的工单 + while next_workorder and next_workorder.state == 'rework': + position += 1 + if position + 1 < len(sorted_workorders): + next_workorder = sorted_workorders[position + 1] + else: + next_workorder = None + if next_workorder and (next_workorder.state == 'ready' or ( next_workorder.state == 'waiting' and next_workorder.is_subcontract)) and cur_workorder.state == 'done': record.back_button_display = True else: @@ -439,7 +446,6 @@ 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 == '表面工艺' and order.state not in ['cancel']: @@ -485,6 +491,7 @@ class ResMrpWorkOrder(models.Model): # 'view_mode': 'tree,form', # }) # return action + def action_view_surface_technics_purchase(self): self.ensure_one() # if self.routing_type == '表面工艺': @@ -513,7 +520,8 @@ class ResMrpWorkOrder(models.Model): return result def _get_surface_technics_purchase_ids(self): - domain = [('origin', 'like', '%' + self.production_id.name + '%'), ('purchase_type', '=', 'consignment'), ('state', '!=', 'cancel')] + domain = [('origin', 'like', '%' + self.production_id.name + '%'), ('purchase_type', '=', 'consignment'), + ('state', '!=', 'cancel')] # domain = [('origin', 'like', '%' + self.production_id.name + '%'), ('purchase_type', '=', 'consignment')] # domain = [('group_id', '=', self.production_id.procurement_group_id.id), ('purchase_type', '=', 'consignment')] purchase_orders = self.env['purchase.order'].search(domain, order='id desc') @@ -736,21 +744,25 @@ class ResMrpWorkOrder(models.Model): # 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") - tomorrow_start = tomorrow + ' 00:00:00' - tomorrow_end = tomorrow + ' 23:59:59' + tomorrow = (date.today() + timedelta(days=1)).strftime("%Y-%m-%d") + tomorrow_start = f"{tomorrow} 00:00:00" + tomorrow_end = f"{tomorrow} 23:59:59" logging.info('tomorrow:%s' % tomorrow) sql = """ SELECT * FROM mrp_workorder WHERE state!='rework' - to_char(date_planned_start::timestamp + '8 hour','YYYY-MM-DD HH:mm:SS')>= %s - AND to_char(date_planned_finished::timestamp + '8 hour','YYYY-MM-DD HH:mm:SS')<= %s + AND (date_planned_start + interval '8 hours') >= %s::timestamp + AND (date_planned_finished + interval '8 hours') <= %s::timestamp """ params = [tomorrow_start, tomorrow_end] if production_line: + line = self.env['sf.production.line'].search( + [('name', '=', production_line)], limit=1) + if not line: + raise ValueError(f"生产线'{production_line}'不存在") sql += "AND production_line_id = %s" - params.append(production_line) + params.append(line.id) self.env.cr.execute(sql, params) ids = [t[0] for t in self.env.cr.fetchall()] return [('id', 'in', ids)] @@ -1243,6 +1255,13 @@ class ResMrpWorkOrder(models.Model): }] return workorders_values_str + # def check_lot_exists(self, picking_id, lot_id): + # return bool( + # picking_id.move_ids.move_line_ids.filtered( + # lambda line: line.lot_id.id == lot_id + # ) + # ) + def _process_compute_state(self): sorted_workorders = sorted(self, key=lambda x: x.sequence) for workorder in sorted_workorders: @@ -1264,10 +1283,17 @@ class ResMrpWorkOrder(models.Model): workorder.state = 'pending' continue # ================= 如果制造订单制造类型为【人工线下加工】========================== + # lot_id = workorder.production_id.move_raw_ids.move_line_ids.lot_id + # picking_ids = workorder.production_id.picking_ids.filtered( + # lambda wk: wk.location_id.name == '外协收料区' and wk.location_dest_id.name == '制造前') + # exists = any( + # move_line.lot_id == lot_id + # for picking in picking_ids + # for move in picking.move_ids + # for move_line in move.move_line_ids + # ) if (workorder.production_id.production_type == '人工线下加工' - and workorder.production_id.schedule_state == '已排' - and len(workorder.production_id.picking_ids.filtered( - lambda w: w.state not in ['done', 'cancel'])) == 0): + and workorder.production_id.schedule_state == '已排'): # and workorder.production_id.programming_state == '已编程' if workorder.is_subcontract is True: if workorder.production_id.state == 'rework': @@ -1276,6 +1302,9 @@ class ResMrpWorkOrder(models.Model): purchase_orders_id = self._get_surface_technics_purchase_ids() if purchase_orders_id.state == 'purchase': workorder.state = 'ready' + # picking_id = workorder.production_id.picking_ids.filtered( + # lambda wk: wk.location_id.name == '制造前' and wk.location_dest_id.name == '外协加工区') + # move_out = picking_id.move_ids move_out = workorder.move_subcontract_workorder_ids[1] for mo in move_out: if mo.state != 'done': @@ -1316,6 +1345,10 @@ class ResMrpWorkOrder(models.Model): if purchase_orders_id.state == 'purchase': workorder.state = 'ready' move_out = workorder.move_subcontract_workorder_ids[1] + # picking_id = workorder.production_id.picking_ids.filtered( + # lambda + # wk: wk.location_id.name == '制造前' and wk.location_dest_id.name == '外协加工区') + # move_out = picking_id.move_ids for mo in move_out: if mo.state != 'done': mo.write({'state': 'assigned', 'production_id': False}) @@ -1325,7 +1358,6 @@ class ResMrpWorkOrder(models.Model): else: workorder.state = 'waiting' - @api.depends('production_availability', 'blocked_by_workorder_ids', 'blocked_by_workorder_ids.state', 'production_id.tool_state', 'production_id.schedule_state', 'sequence', 'production_id.programming_state') @@ -1357,7 +1389,8 @@ class ResMrpWorkOrder(models.Model): # 判断是否有坯料的序列号信息 boolean = False if self.production_id.move_raw_ids: - if self.production_id.move_raw_ids[0].product_id.categ_type == '坯料' and self.production_id.move_raw_ids[0].product_id.tracking == 'serial': + if self.production_id.move_raw_ids[0].product_id.categ_type == '坯料' and \ + self.production_id.move_raw_ids[0].product_id.tracking == 'serial': if self.production_id.move_raw_ids[0].move_line_ids: if self.production_id.move_raw_ids[0].move_line_ids[0].lot_id.name: boolean = True @@ -1390,6 +1423,10 @@ class ResMrpWorkOrder(models.Model): if self.routing_type == '表面工艺': if self.is_subcontract is True: move_out = self.move_subcontract_workorder_ids[1] + # picking_id = self.production_id.picking_ids.filtered( + # lambda wk: wk.location_id.name == '制造前' and wk.location_dest_id.name == '外协加工区') + # move_out = picking_id.move_ids + # move_out = self.move_subcontract_workorder_ids[1] # move_out = self.env['stock.move'].search( # [('location_id', '=', self.env['stock.location'].search( # [('barcode', 'ilike', 'WH-PREPRODUCTION')]).id), @@ -1555,25 +1592,17 @@ class ResMrpWorkOrder(models.Model): len(done_workorder) == len( record.production_id.workorder_ids.filtered(lambda wo: wo.state != 'cancel'))): is_production_id = True - if record.routing_type in ['解除装夹'] or ( - record.is_rework is True and record.routing_type in ['装夹预调']): - for workorder in record.production_id.workorder_ids: - if workorder.processing_panel == record.processing_panel: - rfid_code = workorder.rfid_code - if record.is_rework is not True: - workorder.write({'rfid_code_old': rfid_code, 'rfid_code': False}) - elif workorder.routing_type != '装夹预调' and workorder.state != 'rework': - workorder.write({'rfid_code_old': False, 'rfid_code': False}) - elif workorder.routing_type == '装夹预调' and workorder.state != 'rework': - workorder.write({'rfid_code_old': rfid_code, 'rfid_code': False}) - self.env['stock.lot'].sudo().search([('rfid', '=', rfid_code)]).write( - {'tool_material_status': '可用'}) - if workorder.rfid_code: - raise ValidationError(f'【{workorder.name}】工单解绑失败,请重新点击完成按钮!!!') - # workorder.rfid_code_old = rfid_code - # workorder.rfid_code = False - logging.info('workorder.rfid_code:%s' % workorder.rfid_code) + if record.routing_type in ['解除装夹']: + rfid_code = record.rfid_code + work_ids = record.production_id.workorder_ids.filtered( + lambda wo: wo.processing_panel == record.processing_panel and wo.state != 'rework') + work_ids.write({'rfid_code_old': rfid_code, 'rfid_code': False}) + self.env['stock.lot'].sudo().search([('rfid', '=', rfid_code)]).write( + {'tool_material_status': '可用'}) + if any(wo.rfid_code for wo in work_ids): + raise ValidationError(f'【{record.name}】工单解绑失败,请重新点击完成按钮!!!') + logging.info('work_ids.rfid_code:%s' % [wo.rfid_code for wo in work_ids]) if is_production_id is True: logging.info('product_qty:%s' % record.production_id.product_qty) for move_raw_id in record.production_id.move_raw_ids: @@ -1808,7 +1837,7 @@ class ResMrpWorkOrder(models.Model): orderby=orderby, lazy=lazy ) - + model_id = fields.Char('模型ID', related='production_id.model_id') diff --git a/sf_manufacturing/models/product_template.py b/sf_manufacturing/models/product_template.py index 4bfedf82..19bce23f 100644 --- a/sf_manufacturing/models/product_template.py +++ b/sf_manufacturing/models/product_template.py @@ -795,7 +795,7 @@ class ResProductMo(models.Model): for record in self: if record.categ_id.name == '坯料': product_name = '' - match = re.search(r'(S\d{5}-\d)', record.name) + match = re.search(r'(S\d{5}-\d+)', record.name) # 如果匹配成功,提取结果 if match: product_name = match.group(0) diff --git a/sf_manufacturing/models/purchase_order.py b/sf_manufacturing/models/purchase_order.py index 111c09e3..14e18385 100644 --- a/sf_manufacturing/models/purchase_order.py +++ b/sf_manufacturing/models/purchase_order.py @@ -59,6 +59,86 @@ class PurchaseOrder(models.Model): production_id = self.env['mrp.production'].search([('origin', 'in', origins)]) purchase.production_count = len(production_id) + # def process_replenish(self,production,total_qty): + # record = self + # bom_line_id = production.bom_id.bom_line_ids + # replenish = self.env['stock.warehouse.orderpoint'].search([ + # ('product_id', '=', bom_line_id.product_id.id), + # ( + # 'location_id', '=', self.env.ref('sf_stock.stock_location_outsourcing_material_receiving_area').id), + # # ('state', 'in', ['draft', 'confirmed']) + # ], limit=1) + # if not replenish: + # replenish_model = self.env['stock.warehouse.orderpoint'] + # replenish = replenish_model.create({ + # 'product_id': bom_line_id.product_id.id, + # 'location_id': self.env.ref( + # 'sf_stock.stock_location_outsourcing_material_receiving_area').id, + # 'route_id': self.env.ref('sf_stock.stock_route_process_outsourcing').id, + # 'group_id': record.group_id.id, + # 'qty_to_order': total_qty, + # 'origin': record.name, + # }) + # else: + # replenish.write({ + # 'product_id': bom_line_id.product_id.id, + # 'location_id': self.env.ref( + # 'sf_stock.stock_location_outsourcing_material_receiving_area').id, + # 'route_id': self.env.ref('sf_stock.stock_route_process_outsourcing').id, + # 'group_id': record.group_id.id, + # 'qty_to_order': total_qty + replenish.qty_to_order, + # 'origin': record.name + ',' + replenish.origin, + # }) + # replenish.action_replenish() + + # def outsourcing_service_replenishment(self): + # record = self + # if record.purchase_type != 'consignment': + # return + # grouped_lines = {} + # for line in record.order_line: + # if line.related_product.id not in grouped_lines: + # grouped_lines[line.related_product.id] = [] + # grouped_lines[line.related_product.id].append(line) + # for product_id,lines in grouped_lines.items(): + # production = self.env['mrp.production'].search([('product_id', '=', product_id)], limit=1) + # if not production: + # continue + # total_qty = sum(line.product_qty for line in lines) + # record.process_replenish(production,total_qty) + # for product_id,lines in grouped_lines.items(): + # productions = self.env['mrp.production'].search([('product_id', '=', product_id)], limit=1) + # if not productions: + # continue + # # production.bom_id.bom_line_ids.product_id + # location_id = self.env['stock.location'].search([('name', '=', '制造前')]) + # quants = self.env['stock.quant'].search([ + # ('product_id', '=', productions.bom_id.bom_line_ids.product_id.id), + # ('location_id', '=', location_id.id) + # ]) + # total_qty = sum(quants.mapped('quantity')) # 计算该位置的总库存量 + # is_available = total_qty > 0 + # if not is_available: + # raise UserError('请先完成坯料入库') + # for production_id in productions: + # work_ids = production_id.workorder_ids.filtered( + # lambda wk: wk.state not in ['done', 'rework', 'cancel']) + # if not work_ids: + # continue + # min_sequence_wk = min(work_ids, key=lambda wk: wk.sequence) + # if min_sequence_wk.is_subcontract: + # picking_id = production_id.picking_ids.filtered( + # lambda wk: wk.location_id.name == '制造前' and wk.location_dest_id.name == '外协加工区') + # move_out = picking_id.move_ids + # for mo in move_out: + # if mo.state != 'done': + # mo.write({'state': 'assigned', 'production_id': False}) + # if not mo.move_line_ids: + # self.env['stock.move.line'].create( + # mo.get_move_line(production_id, min_sequence_wk)) + # product = self.env['mrp.production'].search([('product_id', '=', product_id)], limit=1) + # match = re.search(r'(S\d{5}-\d)',product.name) + # pass def button_confirm(self): for record in self: for line in record.order_line: @@ -66,37 +146,10 @@ class PurchaseOrder(models.Model): raise UserError('请对【产品】中的【数量】进行输入') if line.price_unit <= 0: raise UserError('请对【产品】中的【单价】进行输入') - # if record.purchase_type == 'consignment': - # bom_line_id = record.order_line[0].purchase_request_lines.request_id.bom_id.bom_line_ids - # replenish = self.env['stock.warehouse.orderpoint'].search([ - # ('product_id', '=', bom_line_id.product_id.id), - # ( - # 'location_id', '=', self.env.ref('sf_stock.stock_location_outsourcing_material_receiving_area').id), - # # ('state', 'in', ['draft', 'confirmed']) - # ], limit=1) - # if not replenish: - # replenish_model = self.env['stock.warehouse.orderpoint'] - # replenish = replenish_model.create({ - # 'product_id': bom_line_id.product_id.id, - # 'location_id': self.env.ref( - # 'sf_stock.stock_location_outsourcing_material_receiving_area').id, - # 'route_id': self.env.ref('sf_stock.stock_route_process_outsourcing').id, - # 'group_id': record.group_id.id, - # 'qty_to_order': 1, - # 'origin': record.name, - # }) - # else: - # replenish.write({ - # 'product_id': bom_line_id.product_id.id, - # 'location_id': self.env.ref( - # 'sf_stock.stock_location_outsourcing_material_receiving_area').id, - # 'route_id': self.env.ref('sf_stock.stock_route_process_outsourcing').id, - # 'group_id': record.group_id.id, - # 'qty_to_order': 1 + replenish.qty_to_order, - # 'origin': record.name + ',' + replenish.origin, - # }) - # replenish.action_replenish() + # record.outsourcing_service_replenishment() + res = super(PurchaseOrder, self).button_confirm() + for line in self.order_line: # 将产品不追踪序列号的行项目设置qty_done if line.move_ids and line.move_ids[0].product_id.tracking == 'none': @@ -155,7 +208,7 @@ class PurchaseOrderLine(models.Model): continue if record.product_id.categ_id.name == '坯料': product_name = '' - match = re.search(r'(S\d{5}-\d)', record.product_id.name) + match = re.search(r'(S\d{5}-\d+)', record.product_id.name) # 如果匹配成功,提取结果 if match: product_name = match.group(0) diff --git a/sf_manufacturing/models/purchase_request_line.py b/sf_manufacturing/models/purchase_request_line.py index eca52d3b..487683a0 100644 --- a/sf_manufacturing/models/purchase_request_line.py +++ b/sf_manufacturing/models/purchase_request_line.py @@ -1,4 +1,4 @@ -# # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # import base64 # import datetime # import logging @@ -7,24 +7,24 @@ # import re # import traceback # from operator import itemgetter -# + # import requests # from itertools import groupby # from collections import defaultdict, namedtuple -# + # from odoo import api, fields, models, SUPERUSER_ID, _ # from odoo.exceptions import UserError, ValidationError # from odoo.tools import float_compare, float_round, float_is_zero, format_datetime -# -# + + # class PurchaseRequestLine(models.Model): # _inherit = 'purchase.request' # is_subcontract = fields.Boolean(string='是否外协',default=False) # class PurchaseRequestLine(models.Model): # _inherit = 'purchase.request.line' # is_subcontract = fields.Boolean(string='是否外协') -# -# + + # class PurchaseRequest(models.Model): # _inherit = 'purchase.request' # bom_id = fields.Many2one('mrp.bom') diff --git a/sf_manufacturing/models/sale_order.py b/sf_manufacturing/models/sale_order.py index 4ce8750c..04933064 100644 --- a/sf_manufacturing/models/sale_order.py +++ b/sf_manufacturing/models/sale_order.py @@ -58,8 +58,8 @@ class SaleOrder(models.Model): # 复制成品模板上的属性 line.product_id.product_tmpl_id.copy_template(product_template_id) # 将模板上的single_manufacturing属性复制到成品上 - line.product_id.single_manufacturing = product_template_id.single_manufacturing - line.product_id.tracking = product_template_id.tracking + # line.product_id.single_manufacturing = product_template_id.single_manufacturing + # line.product_id.tracking = product_template_id.tracking order_id = self product = line.product_id @@ -76,7 +76,7 @@ class SaleOrder(models.Model): 'embryo_redundancy_id': line.embryo_redundancy_id, } product_name = '' - match = re.search(r'(S\d{5}-\d)', product.name) + match = re.search(r'(S\d{5}-\d+)', product.name) # 如果匹配成功,提取结果 if match: product_name = match.group(0) @@ -190,7 +190,30 @@ class SaleOrder(models.Model): 'target': 'new', 'res_id': wizard.id, } + def create_sale_documents(self, contract_file_name, contract_file): + # 创建ir.attachment记录 + attachment = self.env['ir.attachment'].sudo().create({ + 'name': contract_file_name, + 'type': 'binary', + 'datas': contract_file, + 'res_model': 'sale.order', + }) + # 获取默认的文档文件夹 + workspace = self.env.ref('sf_sale.documents_sales_contracts_folder_1').id + + # 创建 documents.document 记录 + document = self.env['documents.document'].sudo().create({ + 'name': contract_file_name, + 'attachment_id': attachment.id, + 'folder_id': workspace, + 'res_model': 'sale.order', + 'res_id': self.id, + }) + + self.write({ + 'contract_document_id': document.id + }) class SaleOrderLine(models.Model): _inherit = 'sale.order.line' diff --git a/sf_manufacturing/models/sf_production_common.py b/sf_manufacturing/models/sf_production_common.py index 2cdfd750..203dd221 100644 --- a/sf_manufacturing/models/sf_production_common.py +++ b/sf_manufacturing/models/sf_production_common.py @@ -20,13 +20,13 @@ class SfProductionProcessParameter(models.Model): # is_product_button = fields.Boolean(compute='_compute_is_product_button',default=False) # is_delete_button = fields.Boolean(compute='_compute_is_delete_button', default=False) # routing_id = fields.Many2one('mrp.routing.workcenter', string="工序") - # + # @api.depends('outsourced_service_products') # def _compute_service_products(self): # for record in self: # # 假设取第一条作为主明细 - # record.service_products = record.outsourced_service_products.id if record.outsourced_service_products else False - # + # record.service_products = record.outsourced_service_products.ids if record.outsourced_service_products else False + # def _inverse_service_products(self): # for record in self: # if record.service_products: @@ -45,7 +45,7 @@ class SfProductionProcessParameter(models.Model): # for record in self: # if len(record.outsourced_service_products) > 1: # raise ValidationError("工艺参数不能与多个产品关联") - # + # @api.onchange('outsourced_service_products') # def _onchange_validate_partner_limit(self): # for record in self: @@ -58,7 +58,7 @@ class SfProductionProcessParameter(models.Model): # record.is_product_button = True # else: # record.is_product_button = False - # + # def has_wksp_prefix(self): # """ # 判断字符串是否以WKSP开头(不区分大小写) diff --git a/sf_manufacturing/models/stock.py b/sf_manufacturing/models/stock.py index 8572b620..e947b373 100644 --- a/sf_manufacturing/models/stock.py +++ b/sf_manufacturing/models/stock.py @@ -564,6 +564,13 @@ class StockPicking(models.Model): part_numbers = fields.Char(string="零件图号", compute='_compute_part_info', store=True, index=True) part_names = fields.Char(string="零件名称", compute='_compute_part_info', store=True, index=True) + model_id = fields.Char('模型ID', compute='_compute_model_id', store=True, index=True) + + @api.depends('move_ids_without_package.model_id') + def _compute_model_id(self): + for picking in self: + model_id = picking.move_ids_without_package.mapped('model_id') + picking.model_id = ','.join(filter(None, model_id)) @api.depends('move_ids_without_package.part_number', 'move_ids_without_package.part_name') def _compute_part_info(self): @@ -631,6 +638,62 @@ class StockPicking(models.Model): move.action_clear_lines_show_details() move.action_show_details() res = super().button_validate() + # lot_ids = None + # product_ids = self.move_ids.mapped('product_id') + # if not self.move_ids[0].product_id.single_manufacturing and self.move_ids[0].product_id.tracking == 'none': + # lot_ids = self.move_ids.move_line_ids.mapped('lot_id') + # production_ids = self.sale_order_id.mrp_production_ids if self.sale_order_id else self.env['mrp.production'] + # if res and self.location_id.name == '外协收料区' and self.location_dest_id.name == '制造前': + # # 如果是最后一张外协入库单,则设置库存位置的预留数量 + # for production_id in production_ids: + # if lot_ids: + # lot_id = production_id.move_raw_ids.move_line_ids.lot_id + # # picking_ids = production_id.picking_ids.filtered( + # # lambda wk: wk.location_id.name == '外协收料区' and wk.location_dest_id.name == '制造前') + # if lot_id in lot_ids: + # workorder_id = production_id.workorder_ids.filtered( + # lambda a: a.state == 'progress' and a.is_subcontract) + # if not workorder_id: + # continue + # workorder_id.button_finish() + # else: + # workorder_id = production_id.workorder_ids.filtered(lambda a: a.state == 'progress' and a.is_subcontract) + # if not workorder_id: + # continue + # workorder_id.button_finish() + # # lot_id = workorder.production_id.move_raw_ids.move_line_ids.lot_id + # # picking_ids = workorder.production_id.picking_ids.filtered( + # # lambda wk: wk.location_id.name == '外协收料区' and wk.location_dest_id.name == '制造前') + + # # if move_in: + # # workorder = move_in.subcontract_workorder_id + # # workorders = workorder.production_id.workorder_ids + # # subcontract_workorders = workorders.filtered( + # # lambda wo: wo.is_subcontract == True and wo.state != 'cancel').sorted('sequence') + # # # if workorder == subcontract_workorders[-1]: + # # # self.env['stock.quant']._update_reserved_quantity( + # # # move_in.product_id, move_in.location_dest_id, move_in.product_uom_qty, + # # # lot_id=move_in.move_line_ids.lot_id, + # # # package_id=False, owner_id=False, strict=False + # # # ) + # # workorder.button_finish() + # if res and self.location_id.name == '制造前' and self.location_dest_id.name == '外协加工区': + # for production_id in production_ids: + # if lot_ids: + # lot_id = production_id.move_raw_ids.move_line_ids.lot_id + # # picking_ids = production_id.picking_ids.filtered( + # # lambda wk: wk.location_id.name == '外协收料区' and wk.location_dest_id.name == '制造前') + # if lot_id in lot_ids: + # workorder_id = production_id.workorder_ids.filtered( + # lambda a: a.state == 'progress' and a.is_subcontract) + # if not workorder_id: + # continue + # workorder_id.button_finish() + # else: + # workorder_id = production_id.workorder_ids.filtered(lambda a: a.state == 'ready' and a.is_subcontract) + # if not workorder_id: + # continue + # workorder_id.button_start() picking_type_in = self.env.ref('sf_manufacturing.outcontract_picking_in').id if res is True and self.picking_type_id.id == picking_type_in: # 如果是最后一张外协入库单,则设置库存位置的预留数量 @@ -783,6 +846,7 @@ class ReStockMove(models.Model): materiel_height = fields.Float(string='物料高度', digits=(16, 4)) part_number = fields.Char(string='零件图号', compute='_compute_part_info', store=True) part_name = fields.Char(string='零件名称', compute='_compute_part_info', store=True) + model_id = fields.Char('模型ID', related='product_id.model_id') @api.depends('product_id') def _compute_part_info(self): @@ -793,7 +857,7 @@ class ReStockMove(models.Model): move.part_name = move.product_id.part_name elif move.product_id.categ_id.type == '坯料': product_name = '' - match = re.search(r'(S\d{5}-\d)', move.product_id.name) + match = re.search(r'(S\d{5}-\d+)', move.product_id.name) # 如果匹配成功,提取结果 if match: product_name = match.group(0) @@ -825,7 +889,7 @@ class ReStockMove(models.Model): continue product_name = '' logging.info('制造订单的产品 %s', production_id.product_id.name) - match = re.search(r'(S\d{5}-\d)', production_id.product_id.name) + match = re.search(r'(S\d{5}-\d+)', production_id.product_id.name) # 如果匹配成功,提取结果 if match: product_name = match.group(0) diff --git a/sf_manufacturing/models/stock_warehouse_orderpoint.py b/sf_manufacturing/models/stock_warehouse_orderpoint.py new file mode 100644 index 00000000..09289f52 --- /dev/null +++ b/sf_manufacturing/models/stock_warehouse_orderpoint.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of SmartGo. See LICENSE file for full copyright and licensing details. +import base64 +from io import BytesIO +from odoo import api, fields, models, SUPERUSER_ID, _ + + +class StockWarehouseOrderpoint(models.Model): + _inherit = 'stock.warehouse.orderpoint' + origin = fields.Char(string='来源') + _order = 'create_date DESC' \ No newline at end of file diff --git a/sf_manufacturing/views/mrp_production_addional_change.xml b/sf_manufacturing/views/mrp_production_addional_change.xml index 89c92117..abf83f76 100644 --- a/sf_manufacturing/views/mrp_production_addional_change.xml +++ b/sf_manufacturing/views/mrp_production_addional_change.xml @@ -427,6 +427,7 @@ + @@ -602,6 +603,7 @@ + 1 diff --git a/sf_manufacturing/views/mrp_routing_workcenter_view.xml b/sf_manufacturing/views/mrp_routing_workcenter_view.xml index d23e1616..98968058 100644 --- a/sf_manufacturing/views/mrp_routing_workcenter_view.xml +++ b/sf_manufacturing/views/mrp_routing_workcenter_view.xml @@ -22,26 +22,26 @@ - - - - - - - - - - - - - - - - - - - - + + + diff --git a/sf_manufacturing/views/mrp_workorder_view.xml b/sf_manufacturing/views/mrp_workorder_view.xml index c519ff70..4c4bfc2b 100644 --- a/sf_manufacturing/views/mrp_workorder_view.xml +++ b/sf_manufacturing/views/mrp_workorder_view.xml @@ -144,17 +144,17 @@ statusbar_visible="pending,waiting,ready,progress,to be detected,done,rework"/> - - - - - - - - - - - +