diff --git a/quality_control/__init__.py b/quality_control/__init__.py index 7f2c5850..44659a6d 100644 --- a/quality_control/__init__.py +++ b/quality_control/__init__.py @@ -4,3 +4,4 @@ from . import models from . import wizard from . import report +from . import controllers diff --git a/quality_control/__manifest__.py b/quality_control/__manifest__.py index 59fbbb98..ff899d4a 100644 --- a/quality_control/__manifest__.py +++ b/quality_control/__manifest__.py @@ -8,7 +8,7 @@ 'sequence': 120, 'summary': 'Control the quality of your products', 'website': 'https://www.odoo.com/app/quality', - 'depends': ['quality', 'sf_manufacturing'], + 'depends': ['quality', 'sf_manufacturing', 'base_import'], 'description': """ Quality Control =============== @@ -20,12 +20,15 @@ Quality Control """, 'data': [ 'data/quality_control_data.xml', + 'wizard/import_complex_model.xml', + 'wizard/quality_wizard_view.xml', 'report/worksheet_custom_reports.xml', 'report/worksheet_custom_report_templates.xml', 'views/quality_views.xml', 'views/product_views.xml', 'views/stock_move_views.xml', 'views/stock_picking_views.xml', + 'views/quality.check.measures.line.xml', 'wizard/quality_check_wizard_views.xml', 'security/ir.model.access.csv', ], diff --git a/quality_control/controllers/__init__.py b/quality_control/controllers/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/quality_control/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/quality_control/controllers/main.py b/quality_control/controllers/main.py new file mode 100644 index 00000000..5bbe71a9 --- /dev/null +++ b/quality_control/controllers/main.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +from odoo import http +from odoo.http import request, Response +import base64 +import json + + +class QualityController(http.Controller): + + @http.route(['/api/quality/report/download'], type='http', auth='public', csrf=False, website=False) # 移除 cors="*" + def get_quality_report(self, retrospect_ref=None, **kwargs): + """获取质检报告的下载接口 + + Args: + retrospect_ref: 追溯码 + + Returns: + 直接返回文件下载响应 + """ + try: + # 如果retrospect_ref为None,尝试从查询参数获取 + if not retrospect_ref: + retrospect_ref = kwargs.get('retrospect_ref') + + # 参数验证 + if not retrospect_ref: + return self._json_response({ + 'status': 'error', + 'message': '追溯码不能为空' + }) + + # 查找对应的质检单 + quality_check = request.env['quality.check'].sudo().search([ + ('picking_id.retrospect_ref', '=', retrospect_ref), + ('publish_status', '=', 'published') # 只返回已发布的报告 + ], limit=1) + + if not quality_check: + return self._json_response({ + 'status': 'error', + 'message': '未找到对应的质检报告或报告未发布' + }) + + if not quality_check.report_number_id: + return self._json_response({ + 'status': 'error', + 'message': '质检报告文件不存在' + }) + + # 获取文件内容 + document = quality_check.report_number_id + if not document.raw: # 检查文件内容是否存在 + return self._json_response({ + 'status': 'error', + 'message': '文件内容不存在' + }) + + # 构建文件名(确保有.pdf后缀) + filename = document.name + if not filename.lower().endswith('.pdf'): + filename = f"{filename}.pdf" + + # 返回文件下载响应 + return Response( + document.raw, + headers=[ + ('Content-Type', 'application/pdf'), + ('Content-Disposition', f'attachment; filename="{filename}"'), + ('Access-Control-Allow-Origin', '*'), + ('Access-Control-Allow-Methods', 'GET, OPTIONS'), + ('Access-Control-Allow-Headers', 'Content-Type, Authorization') + ] + ) + + except Exception as e: + return self._json_response({ + 'status': 'error', + 'message': f'系统错误: {str(e)}' + }) + + def _json_response(self, data): + """返回JSON格式的响应""" + return Response( + json.dumps(data, ensure_ascii=False), + mimetype='application/json;charset=utf-8', + headers=[ + ('Access-Control-Allow-Origin', '*'), + ('Access-Control-Allow-Methods', 'GET, OPTIONS'), + ('Access-Control-Allow-Headers', 'Content-Type, Authorization') + ] + ) + +class QualityReportController(http.Controller): + + @http.route('/quality/report/', type='http', auth='public') + def get_public_report(self, attachment_id, **kw): + """提供公开访问PDF报告的控制器""" + attachment = request.env['ir.attachment'].sudo().browse(int(attachment_id)) + + # 安全检查:确保只有质检报告附件可以被访问 + if attachment.exists() and 'QC-' in attachment.name: + # 解码Base64数据为二进制数据 + pdf_content = base64.b64decode(attachment.datas) + + # 返回解码后的PDF内容 + return request.make_response( + pdf_content, + headers=[ + ('Content-Type', 'application/pdf'), + ('Content-Disposition', f'inline; filename={attachment.name}') + ] + ) + return request.not_found() \ No newline at end of file diff --git a/quality_control/models/quality.py b/quality_control/models/quality.py index 011c7914..961bd2a7 100644 --- a/quality_control/models/quality.py +++ b/quality_control/models/quality.py @@ -10,6 +10,12 @@ from odoo import api, models, fields, _ from odoo.api import depends from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_round from odoo.osv.expression import OR +from odoo.exceptions import UserError +from odoo.tools import image_data_uri +from base64 import b64encode +import requests +import json +import base64 class QualityPoint(models.Model): @@ -34,7 +40,8 @@ class QualityPoint(models.Model): ('day', 'Days'), ('week', 'Weeks'), ('month', 'Months')], default="day") # TDE RENAME ? - is_lot_tested_fractionally = fields.Boolean(string="Lot Tested Fractionally", help="Determines if only a fraction of the lot should be tested") + is_lot_tested_fractionally = fields.Boolean(string="Lot Tested Fractionally", + help="Determines if only a fraction of the lot should be tested") testing_percentage_within_lot = fields.Float(help="Defines the percentage within a lot that should be tested") norm = fields.Float('Norm', digits='Quality Tests') # TDE RENAME ? tolerance_min = fields.Float('Min Tolerance', digits='Quality Tests') @@ -63,7 +70,7 @@ class QualityPoint(models.Model): if n > 1: point.average = mean - point.standard_deviation = sqrt( s / ( n - 1)) + point.standard_deviation = sqrt(s / (n - 1)) elif n == 1: point.average = mean point.standard_deviation = 0.0 @@ -94,7 +101,7 @@ class QualityPoint(models.Model): checks = self.env['quality.check'].search([ ('point_id', '=', self.id), ('create_date', '>=', date_previous.strftime(DEFAULT_SERVER_DATETIME_FORMAT))], limit=1) - return not(bool(checks)) + return not (bool(checks)) return super(QualityPoint, self).check_execute_now() def _get_type_default_domain(self): @@ -123,13 +130,480 @@ class QualityPoint(models.Model): class QualityCheck(models.Model): _inherit = "quality.check" - part_name = fields.Char('零件名称', compute='_compute_part_name_number', readonly=True) - part_number = fields.Char('零件图号', compute='_compute_part_name_number', readonly=True) - @depends('product_id') - def _compute_part_name_number(self): + part_name = fields.Char('零件名称', related='product_id.part_name') + part_number = fields.Char('零件图号', related='product_id.part_number', readonly=False, store=True) + material_name = fields.Char('材料名称', compute='_compute_material_name') + + # # 总数量,值为调拨单_产品明细_数量 + # total_qty = fields.Float('总数量', compute='_compute_total_qty', readonly=True) + # # 检验数 + # check_qty = fields.Float('检验数', compute='_compute_check_qty', readonly=True) + # # 出厂检验报告编号 + # report_number = fields.Char('出厂检验报告编号', compute='_compute_report_number', readonly=True) + # 总数量,值为调拨单_产品明细_数量 + total_qty = fields.Char('总数量', compute='_compute_total_qty') + + column_nums = fields.Integer('测量值列数', default=1) + + @api.depends('picking_id') + def _compute_total_qty(self): for record in self: - record.part_number = record.product_id.part_number - record.part_name = record.product_id.part_name + if record.picking_id: + total_qty = 0 + for move in record.picking_id.move_ids_without_package: + total_qty += move.quantity_done + record.total_qty = total_qty if total_qty > 0 else '' + else: + record.total_qty = '' + + # 检验数 + check_qty = fields.Integer('检验数', default=lambda self: self._get_default_check_qty()) + + def _get_default_check_qty(self): + """根据条件设置检验数的默认值""" + # 这里需要使用_origin来获取已存储的记录,因为新记录在创建时可能还没有这些值 + if self._origin: + if self._origin.measure_on == 'product' and self._origin.test_type_id.name == '出厂检验报告': + return '' + elif self._origin.measure_on == 'product': + return '1' + return '' + + @api.onchange('test_type_id', 'measure_on') + def _onchange_check_qty(self): + """当测试类型或测量对象变化时,更新检验数""" + if self.measure_on == 'product' and self.test_type_id.name == '出厂检验报告': + self.check_qty = 0 + elif self.measure_on == 'product': + self.check_qty = 1 + + # 出厂检验报告编号 + report_number_id = fields.Many2one('documents.document', string='出厂检验报告编号', readonly=True) + + old_report_name = fields.Char('旧出厂检验报告编号', default='') + + # 出厂检验报告、关联文档的数据 + report_content = fields.Binary(string='出厂检验报告', related='report_number_id.datas') + + is_out_check = fields.Boolean(string='是否出库检验', compute='_compute_is_out_check', readonly=True) + + measure_line_ids = fields.One2many('quality.check.measure.line', 'check_id', string='测量明细') + + categ_type = fields.Selection(string='产品的类别', related='product_id.categ_id.type', store=True) + + report_result = fields.Selection([ + ('OK', 'OK'), + ('NG', 'NG') + ], string='出厂检验报告结果', default='OK') + measure_operator = fields.Many2one('res.users', string='操机员') + quality_manager = fields.Many2one('res.users', string='质检员') + + # 流水号(从1开始,最大99) + serial_number = fields.Integer('流水号', default=1, readonly=True) + + # 发布历史 + report_history_ids = fields.One2many('quality.check.report.history', 'check_id', string='发布历史') + + # 发布状态 + publish_status = fields.Selection([ + ('draft', '草稿'), + ('published', '已发布'), + ('canceled', '已撤销') + ], string='发布状态', default='draft') + + # 出厂检验报告是否已上传 + is_factory_report_uploaded = fields.Boolean(string='出厂检验报告是否已上传', default=False) + + def add_measure_line(self): + """ + 新增测量值,如果测量值有5列了,则提示“最多只能有5列测量值” + """ + if self.column_nums >= 5: + raise UserError(_('最多只能有5列测量值')) + else: + for line in self.measure_line_ids: + field_name = f'measure_value{self.column_nums + 1}' + if hasattr(line, field_name): + line[field_name] = False + self.column_nums = self.column_nums + 1 + + def remove_measure_line(self): + """ + 删除测量值 + """ + if self.column_nums <= 1: + raise UserError(_('最少要有1列测量值')) + else: + for line in self.measure_line_ids: + field_name = f'measure_value{self.column_nums}' + if hasattr(line, field_name): + line[field_name] = False + self.column_nums = self.column_nums - 1 + + def do_preview(self): + """ + 预览出厂检验报告 + """ + pass + + def do_publish(self): + """发布出厂检验报告""" + self.ensure_one() + self._check_part_number() + self._check_measure_line() + self._check_check_qty_and_total_qty() + + # 打开确认向导而不是直接发布 + return { + 'name': _('发布确认'), + 'type': 'ir.actions.act_window', + 'res_model': 'quality.check.publish.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_check_id': self.id, + 'default_product_name': self.product_id.name, + 'default_total_qty': self.total_qty, + 'default_check_qty': self.check_qty, + 'default_measure_count': self.column_nums, + 'default_item_count': len(self.measure_line_ids), + 'default_old_report_name': self.old_report_name, + 'default_publish_status': self.publish_status, + } + } + + def _do_publish_implementation(self): + """实际执行发布操作的方法""" + self.ensure_one() + + # 1. 获取报告动作 + report_action = self.env.ref('sf_quality.action_report_quality_inspection') + + # 2. 生成PDF报告 - 修改这里的调用方式 + pdf_content, _ = report_action._render_qweb_pdf( + report_ref=report_action.report_name, # 添加report_ref参数 + res_ids=self.ids + ) + + # attachment = self.env['ir.attachment'].create({ + # 'name': f'{self.name}.pdf', + # 'type': 'binary', + # 'datas': b64encode(pdf_content), + # 'res_model': self._name, + # 'res_id': self.id, + # 'mimetype': 'application/pdf', + # }) + + # 获取已发布的文档文件夹 + workspace = self.env['documents.folder'].search( + [('parent_folder_id', '=', self.env.ref('sf_quality.documents_purchase_contracts_folder').id), + ('name', '=', '已发布')], limit=1) + + if self.serial_number > 99: + raise UserError(_('流水号不能大于99')) + + str_serial_number = '0' + str(self.serial_number) if self.serial_number < 10 else str(self.serial_number) + str_part_number = self.part_number if self.part_number else '' + # 3. 创建文档记录 + doc_vals = { + 'name': f'FQC{str_part_number}{str_serial_number}', + 'raw': pdf_content, + # 'attachment_id': attachment.id, + 'mimetype': 'application/pdf', + 'res_id': self.id, + 'folder_id': workspace.id, + 'res_model': self._name, + } + + doc = self.env['documents.document'].create(doc_vals) + # 关联到当前质检记录 + self.write({ + 'report_number_id': doc.id, + 'quality_state': 'pass' + }) + + # 记录发布历史 + self.env['quality.check.report.history'].create({ + 'check_id': self.id, + 'report_number_id': doc.id, + 'action': 'publish', + 'operator': self.env.user.name, + 'operation_time': datetime.now(), + 'document_status': 'published', + 'sequence': len(self.report_history_ids) + 1 + }) + + # 更新流水号 + self.serial_number += 1 + self.quality_manager = self.env.user.id + + if self.publish_status == 'canceled': + self.upload_factory_report() + + self.write({ + 'publish_status': 'published', + }) + + # 返回成功消息 + return True + + # 发布前检验零件图号、操机员、质检员 + def _check_part_number(self): + if not self.part_number: + raise UserError(_('零件图号不能为空')) + if not self.measure_operator: + raise UserError(_('操机员不能为空')) + + # 发布前校验明细行列均非空 + def _check_measure_line(self): + for record in self: + if not record.measure_line_ids: + raise UserError(_('请先添加测量明细')) + for line in record.measure_line_ids: + if not line.measure_item: + raise UserError(_('有检测项目值为空')) + for i in range(1, record.column_nums + 1): + if not getattr(line, f'measure_value{i}'): + raise UserError(_('有测量值为空')) + + # 发布前校验检验数与总数量、检验数与测量件数(即测量列数) + def _check_check_qty_and_total_qty(self): + for record in self: + if not record.check_qty: + raise UserError(_('请先输入检验数')) + if not record.total_qty: + raise UserError(_('总数量不能为空')) + if record.check_qty <= int(record.total_qty): + raise UserError(_('检验数不可超过总数量')) + if record.column_nums >= record.check_qty: + raise UserError(_('测量件数不可超过检验数')) + + def do_cancel_publish(self): + """ + 取消发布出厂检验报告(将当前质检单关联的出厂检验报告文档位置移动到废弃文件夹), 并记录发布历史 + """ + self.ensure_one() + # 1. 获取已发布的文档文件夹 + workspace = self.env['documents.folder'].search( + [('parent_folder_id', '=', self.env.ref('sf_quality.documents_purchase_contracts_folder').id), + ('name', '=', '已发布')], limit=1) + # 2. 将当前质检单关联的出厂检验报告文档位置移动到废弃文件夹 + self.report_number_id.write({ + 'folder_id': self.env.ref('sf_quality.documents_purchase_contracts_folder_canceled').id, + }) + + # 3. 记录发布历史 + self.env['quality.check.report.history'].create({ + 'check_id': self.id, + 'report_number_id': self.report_number_id.id, + 'action': 'cancel_publish', + 'operator': self.env.user.name, + 'operation_time': datetime.now(), + 'document_status': 'canceled', + 'sequence': len(self.report_history_ids) + 1 + }) + + self.write({ + 'old_report_name': self.report_number_id.name + }) + + # 3. 更新发布状态 + self.write({ + 'publish_status': 'canceled', + 'report_number_id': False, + 'quality_state': 'none' + }) + + # 4. 删除加工订单明细中的出厂检验报告 + self.delete_factory_report() + + return True + + def do_re_publish(self): + """ + 重新发布出厂检验报告,参考发布规则 + """ + return self.do_publish() + + def generate_qr_code(self): + """生成二维码URL""" + self.ensure_one() + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + return image_data_uri( + b64encode(self.env['ir.actions.report'].barcode( + 'QR', base_url + '/#/index/publicPay?order_id=' + str(self.id) + '&source=%2Findex%2Fmyorder', + width=140, height=140) + ) + ) + + def get_latest_report_attachment(self, check_id): + """获取指定质检记录的最新报告附件,并删除旧的报告附件""" + # 查找特定质检记录的所有附件 + attachments = self.env['ir.attachment'].search([ + ('res_model', '=', 'quality.check'), + ('res_id', '=', check_id), + ('name', 'like', 'QC-QC') # 根据您的命名规则调整 + ], order='create_date DESC') # 按创建日期降序排序 + + # # 如果附件数量大于1,则删除除最新报告外的其他报告附件 + # if len(attachments) > 1: + # for attachment in attachments[1:]: + # attachment.unlink() + + # 返回最新的附件(如果存在) + return attachments and attachments[0] or False + + def get_report_url(self): + """生成报告访问URL,确保获取最新版本""" + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + report_url = f"{base_url}/web/content/ir.attachment" + + # 获取最新附件的ID + latest_attachment = self.get_latest_report_attachment(self.id) + if latest_attachment: + # 生成包含附件ID的URL + print(f"{base_url}/quality/report/{latest_attachment.id}") + return f"{base_url}/quality/report/{latest_attachment.id}" + return False + + def upload_factory_report(self): + """ + 上传出厂检验报告到加工订单明细中 + 将当前质检单的出厂检验报告上传到对应的加工订单明细中 + """ + self.ensure_one() + if not self.report_content: + raise UserError(_('当前质检单没有出厂检验报告,请先发布报告')) + + if not self.part_number: + raise UserError(_('零件图号不能为空')) + + if not self.picking_id or not self.picking_id.origin: + raise UserError(_('无法找到相关的调拨单或来源单据')) + + # 获取订单号(从调拨单的来源字段获取) + order_ref = self.picking_id.retrospect_ref + + try: + # 准备请求数据 + payload = { + "order_ref": order_ref, + "part_number": self.part_number, + "report_file": self.report_content.decode('utf-8') if isinstance(self.report_content, bytes) else self.report_content + } + + # 将Python字典转换为JSON字符串 + json_data = json.dumps(payload) + + # 获取服务器URL + base_url = self.env['ir.config_parameter'].sudo().get_param('bfm_url_new') + api_url = f"{base_url}/api/report/create" + + # 设置请求头 + headers = { + 'Content-Type': 'application/json', + } + + # 发送POST请求 + response = requests.post(api_url, data=json_data, headers=headers) + + # 处理响应 + if response.status_code == 200: + result = response.json() + if result.get('success'): + # 上传成功,显示成功消息 + self.is_factory_report_uploaded = True + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('上传成功'), + 'message': _('出厂检验报告已成功上传到加工订单明细'), + 'type': 'success', + 'sticky': False, + } + } + else: + # API返回失败信息 + raise UserError(_('上传失败: %s') % result.get('message', '未知错误')) + else: + # HTTP请求失败 + raise UserError(_('请求失败,状态码: %s') % response.status_code) + + except Exception as e: + raise UserError(_('上传过程中发生错误: %s') % str(e)) + + def delete_factory_report(self): + """ + 删除加工订单明细中的出厂检验报告 + """ + # 获取订单号(从调拨单的来源字段获取) + order_ref = self.picking_id.retrospect_ref + + try: + # 准备请求数据 + payload = { + "order_ref": order_ref, + "part_number": self.part_number + } + + # 将Python字典转换为JSON字符串 + json_data = json.dumps(payload) + + # 获取服务器URL + base_url = self.env['ir.config_parameter'].sudo().get_param('bfm_url_new') + api_url = f"{base_url}/api/report/delete" + + # 设置请求头 + headers = { + 'Content-Type': 'application/json', + } + + # 发送POST请求 + response = requests.post(api_url, data=json_data, headers=headers) + + # 处理响应 + if response.status_code == 200: + result = response.json() + if result.get('success'): + # 删除成功,显示成功消息 + self.is_factory_report_uploaded = False + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('删除成功'), + 'message': _('出厂检验报告已成功删除'), + 'type': 'success', + 'sticky': False, + } + } + else: + # API返回失败信息 + raise UserError(_('删除失败: %s') % result.get('message', '未知错误')) + else: + # HTTP请求失败 + raise UserError(_('请求失败,状态码: %s') % response.status_code) + + except Exception as e: + raise UserError(_('删除过程中发生错误: %s') % str(e)) + + + @depends('product_id') + def _compute_material_name(self): + for record in self: + materials_id_name = record.product_id.materials_id.name if record.product_id.materials_id else '' + materials_type_name = record.product_id.materials_type_id.name if record.product_id.materials_type_id else '' + record.material_name = materials_id_name + ' ' + materials_type_name + + @depends('test_type_id') + def _compute_is_out_check(self): + for record in self: + if record.test_type_id.name == '出厂检验报告': + record.is_out_check = True + else: + record.is_out_check = False + failure_message = fields.Html(related='point_id.failure_message', readonly=True) measure = fields.Float('Measure', default=0.0, digits='Quality Tests', tracking=True) measure_success = fields.Selection([ @@ -141,7 +615,8 @@ class QualityCheck(models.Model): tolerance_max = fields.Float('Max Tolerance', related='point_id.tolerance_max', readonly=True) warning_message = fields.Text(compute='_compute_warning_message') norm_unit = fields.Char(related='point_id.norm_unit', readonly=True) - qty_to_test = fields.Float(compute="_compute_qty_to_test", string="Quantity to Test", help="Quantity of product to test within the lot") + qty_to_test = fields.Float(compute="_compute_qty_to_test", string="Quantity to Test", + help="Quantity of product to test within the lot") qty_tested = fields.Float(string="Quantity Tested", help="Quantity of product tested within the lot") measure_on = fields.Selection([ ('operation', 'Operation'), @@ -150,7 +625,8 @@ class QualityCheck(models.Model): help="""Operation = One quality check is requested at the operation level. Product = A quality check is requested per product. Quantity = A quality check is requested for each new product quantity registered, with partial quantity checks also possible.""") - move_line_id = fields.Many2one('stock.move.line', 'Stock Move Line', check_company=True, help="In case of Quality Check by Quantity, Move Line on which the Quality Check applies") + move_line_id = fields.Many2one('stock.move.line', 'Stock Move Line', check_company=True, + help="In case of Quality Check by Quantity, Move Line on which the Quality Check applies") lot_name = fields.Char('Lot/Serial Number Name') lot_line_id = fields.Many2one('stock.lot', store=True, compute='_compute_lot_line_id') qty_line = fields.Float(compute='_compute_qty_line', string="Quantity") @@ -231,7 +707,8 @@ class QualityCheck(models.Model): def _compute_qty_to_test(self): for qc in self: if qc.is_lot_tested_fractionally: - qc.qty_to_test = float_round(qc.qty_line * qc.testing_percentage_within_lot / 100, precision_rounding=self.product_id.uom_id.rounding, rounding_method="UP") + qc.qty_to_test = float_round(qc.qty_line * qc.testing_percentage_within_lot / 100, + precision_rounding=self.product_id.uom_id.rounding, rounding_method="UP") else: qc.qty_to_test = qc.qty_line @@ -386,7 +863,8 @@ class QualityAlert(models.Model): class ProductTemplate(models.Model): _inherit = "product.template" - quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user') + quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', + groups='quality.group_quality_user') quality_pass_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user') quality_fail_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user') @@ -394,14 +872,16 @@ class ProductTemplate(models.Model): def _compute_quality_check_qty(self): for product_tmpl in self: product_tmpl.quality_fail_qty, product_tmpl.quality_pass_qty = product_tmpl.product_variant_ids._count_quality_checks() - product_tmpl.quality_control_point_qty = product_tmpl.with_context(active_test=product_tmpl.active).product_variant_ids._count_quality_points() + product_tmpl.quality_control_point_qty = product_tmpl.with_context( + active_test=product_tmpl.active).product_variant_ids._count_quality_points() def action_see_quality_control_points(self): self.ensure_one() action = self.env["ir.actions.actions"]._for_xml_id("quality_control.quality_point_action") action['context'] = dict(self.env.context, default_product_ids=self.product_variant_ids.ids) - domain_in_products_or_categs = ['|', ('product_ids', 'in', self.product_variant_ids.ids), ('product_category_ids', 'parent_of', self.categ_id.ids)] + domain_in_products_or_categs = ['|', ('product_ids', 'in', self.product_variant_ids.ids), + ('product_category_ids', 'parent_of', self.categ_id.ids)] domain_no_products_and_categs = [('product_ids', '=', False), ('product_category_ids', '=', False)] action['domain'] = OR([domain_in_products_or_categs, domain_no_products_and_categs]) return action @@ -412,10 +892,10 @@ class ProductTemplate(models.Model): action['context'] = dict(self.env.context, default_product_id=self.product_variant_id.id, create=False) action['domain'] = [ '|', - ('product_id', 'in', self.product_variant_ids.ids), - '&', - ('measure_on', '=', 'operation'), - ('picking_id.move_ids.product_tmpl_id', '=', self.id), + ('product_id', 'in', self.product_variant_ids.ids), + '&', + ('measure_on', '=', 'operation'), + ('picking_id.move_ids.product_tmpl_id', '=', self.id), ] return action @@ -423,7 +903,8 @@ class ProductTemplate(models.Model): class ProductProduct(models.Model): _inherit = "product.product" - quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user') + quality_control_point_qty = fields.Integer(compute='_compute_quality_check_qty', + groups='quality.group_quality_user') quality_pass_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user') quality_fail_qty = fields.Integer(compute='_compute_quality_check_qty', groups='quality.group_quality_user') @@ -437,10 +918,10 @@ class ProductProduct(models.Model): quality_pass_qty = 0 domain = [ '|', - ('product_id', 'in', self.ids), - '&', - ('measure_on', '=', 'operation'), - ('picking_id.move_ids.product_id', 'in', self.ids), + ('product_id', 'in', self.ids), + '&', + ('measure_on', '=', 'operation'), + ('picking_id.move_ids.product_id', 'in', self.ids), ('company_id', '=', self.env.company.id), ('quality_state', '!=', 'none') ] @@ -464,7 +945,8 @@ class ProductProduct(models.Model): _, where_clause, where_clause_args = query.get_sql() additional_where_clause = self._additional_quality_point_where_clause() where_clause += additional_where_clause - parent_category_ids = [int(parent_id) for parent_id in self.categ_id.parent_path.split('/')[:-1]] if self.categ_id else [] + parent_category_ids = [int(parent_id) for parent_id in + self.categ_id.parent_path.split('/')[:-1]] if self.categ_id else [] self.env.cr.execute(""" SELECT COUNT(*) @@ -485,7 +967,7 @@ class ProductProduct(models.Model): ) ) """ % (where_clause,), where_clause_args + [self.ids, parent_category_ids] - ) + ) return self.env.cr.fetchone()[0] def action_see_quality_control_points(self): @@ -493,7 +975,8 @@ class ProductProduct(models.Model): action = self.product_tmpl_id.action_see_quality_control_points() action['context'].update(default_product_ids=self.ids) - domain_in_products_or_categs = ['|', ('product_ids', 'in', self.ids), ('product_category_ids', 'parent_of', self.categ_id.ids)] + domain_in_products_or_categs = ['|', ('product_ids', 'in', self.ids), + ('product_category_ids', 'parent_of', self.categ_id.ids)] domain_no_products_and_categs = [('product_ids', '=', False), ('product_category_ids', '=', False)] action['domain'] = OR([domain_in_products_or_categs, domain_no_products_and_categs]) return action @@ -504,12 +987,75 @@ class ProductProduct(models.Model): action['context'] = dict(self.env.context, default_product_id=self.id, create=False) action['domain'] = [ '|', - ('product_id', '=', self.id), - '&', - ('measure_on', '=', 'operation'), - ('picking_id.move_ids.product_id', '=', self.id), + ('product_id', '=', self.id), + '&', + ('measure_on', '=', 'operation'), + ('picking_id.move_ids.product_id', '=', self.id), ] return action def _additional_quality_point_where_clause(self): return "" + + +class QualityCheckMeasureLine(models.Model): + _name = 'quality.check.measure.line' + _description = '质检测量明细' + _order = 'sequence, id' + + sequence = fields.Integer('序号') + + check_id = fields.Many2one('quality.check', string='质检单', required=True, ondelete='cascade') + + # 基本信息 + product_name = fields.Char('产品名称', related='check_id.product_id.name', readonly=True) + drawing_no = fields.Char('图号') + measure_item = fields.Char('检测项目') + + # 测量值 + measure_value1 = fields.Char('测量值1') + measure_value2 = fields.Char('测量值2') + measure_value3 = fields.Char('测量值3') + measure_value4 = fields.Char('测量值4') + measure_value5 = fields.Char('测量值5') + + # # 展示列数 + # column_nums = fields.Integer('列数', related='check_id.column_nums') + + # 判定结果 + measure_result = fields.Selection([ + ('OK', '合格'), + ('NG', '不合格') + ], string='判定', default='OK') + + remark = fields.Char('备注') + + def del_measure_value(self): + self.ensure_one() + self.sudo().unlink() + + +# 增加出厂检验报告发布历史 +class QualityCheckReportHistory(models.Model): + _name = 'quality.check.report.history' + _description = '出厂检验报告发布历史' + + check_id = fields.Many2one('quality.check', string='质检单', required=True, ondelete='cascade') + report_number_id = fields.Many2one('documents.document', string='报告编号', readonly=True) + + sequence = fields.Integer('序号') + # 操作(发布、撤销发布、重新发布) + action = fields.Selection([ + ('publish', '发布'), + ('cancel_publish', '撤销发布'), + ('re_publish', '重新发布') + ], string='操作') + # 操作人 + operator = fields.Char('操作人') + # 操作时间 + operation_time = fields.Datetime('操作时间') + # 文档状态(已发布、废弃) + document_status = fields.Selection([ + ('published', '已发布'), + ('canceled', '废弃') + ], string='操作后文档状态') diff --git a/quality_control/security/ir.model.access.csv b/quality_control/security/ir.model.access.csv index 9c0c5529..178283e1 100644 --- a/quality_control/security/ir.model.access.csv +++ b/quality_control/security/ir.model.access.csv @@ -1,3 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_quality_check_wizard,access.quality_check_wizard,model_quality_check_wizard,quality.group_quality_user,1,1,1,0 +access_quality_check_measure_line,quality.check.measure.line,model_quality_check_measure_line,base.group_user,1,1,1,0 +access_quality_check_import_complex_model_wizard,quality.check.import.complex.model.wizard,model_quality_check_import_complex_model_wizard,quality.group_quality_user,1,1,1,0 +access_quality_check_report_history,quality.check.report.history,model_quality_check_report_history,quality.group_quality_user,1,1,1,0 +access_quality_check_publish_wizard,quality.check.publish.wizard,model_quality_check_publish_wizard,quality.group_quality_user,1,1,1,0 access_picking_check_cancel_wizard,access.picking_check_cancel_wizard,model_picking_check_cancel_wizard,quality.group_quality_user,1,1,1,0 diff --git a/quality_control/static/src/binary/出厂检验报告上传模版.xlsx b/quality_control/static/src/binary/出厂检验报告上传模版.xlsx new file mode 100644 index 00000000..cd43894a Binary files /dev/null and b/quality_control/static/src/binary/出厂检验报告上传模版.xlsx differ diff --git a/quality_control/views/quality.check.measures.line.xml b/quality_control/views/quality.check.measures.line.xml new file mode 100644 index 00000000..c3cb5353 --- /dev/null +++ b/quality_control/views/quality.check.measures.line.xml @@ -0,0 +1,22 @@ + + + + quality.check.measure.line.tree + quality.check.measure.line + + + + + + + + + + + + +