Merge branch 'develop' into feature/commercially_launched

This commit is contained in:
胡尧
2025-03-17 15:05:47 +08:00
29 changed files with 1621 additions and 46 deletions

View File

@@ -4,3 +4,4 @@
from . import models
from . import wizard
from . import report
from . import controllers

View File

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

View File

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

View File

@@ -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/<int:attachment_id>', 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()

View File

@@ -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='操作后文档状态')

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_quality_check_wizard access.quality_check_wizard model_quality_check_wizard quality.group_quality_user 1 1 1 0
3 access_quality_check_measure_line quality.check.measure.line model_quality_check_measure_line base.group_user 1 1 1 0
4 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
5 access_quality_check_report_history quality.check.report.history model_quality_check_report_history quality.group_quality_user 1 1 1 0
6 access_quality_check_publish_wizard quality.check.publish.wizard model_quality_check_publish_wizard quality.group_quality_user 1 1 1 0
7 access_picking_check_cancel_wizard access.picking_check_cancel_wizard model_picking_check_cancel_wizard quality.group_quality_user 1 1 1 0

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="quality_check_measure_line_tree" model="ir.ui.view">
<field name="name">quality.check.measure.line.tree</field>
<field name="model">quality.check.measure.line</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="sequence"/>
<!-- <field name="column_nums"/> -->
<field name="measure_item"/>
<field name="measure_value1" attrs="{ 'column_invisible': [('parent.column_nums', '&lt;', 1)] }"/>
<field name="measure_value2" attrs="{ 'column_invisible': [('parent.column_nums', '&lt;', 2)] }"/>
<field name="measure_value3" attrs="{ 'column_invisible': [('parent.column_nums', '&lt;', 3)] }"/>
<field name="measure_value4" attrs="{ 'column_invisible': [('parent.column_nums', '&lt;', 4)] }"/>
<field name="measure_value5" attrs="{ 'column_invisible': [('parent.column_nums', '&lt;', 5)] }"/>
<field name="measure_result"/>
<field name="remark"/>
<button name="del_measure_value" type="object" string="删除" class="btn-danger"/>
</tree>
</field>
</record>
</odoo>

View File

@@ -255,6 +255,7 @@
<field name="quality_state" widget="statusbar"/>
</header>
<sheet>
<field name="is_out_check" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button name="action_see_alerts" icon="fa-bell" type="object" class="oe_stat_button"
attrs="{'invisible': [('alert_count', '=', 0)]}">
@@ -264,10 +265,17 @@
<group>
<group>
<field name="company_id" invisible="1"/>
<field name="categ_type" invisible="1"/>
<field name="product_id" attrs="{'invisible' : [('measure_on', '=', 'operation')]}"/>
<field name="measure_on" attrs="{'readonly': [('point_id', '!=', False)]}"/>
<field name="part_name"/>
<field name="part_number"/>
<field name="part_name" attrs="{'invisible': [('categ_type', '!=', '成品')]}"/>
<field name="part_number" attrs="{'invisible': [('categ_type', '!=', '成品')]}"/>
<field name="material_name" attrs="{'invisible': [('categ_type', '!=', '成品')]}"/>
<field name="total_qty" attrs="{'invisible': ['|', ('measure_on', '!=', 'product'), ('is_out_check', '=', False)]}"/>
<field name="check_qty" attrs="{'invisible': [('measure_on', '!=', 'product')]}"/>
<!-- <field name="categ_type"/> -->
<field name="report_number_id"/>
<field name="column_nums" invisible="1"/>
<field name="publish_status"/>
<field name="show_lot_text" invisible="1"/>
<field name="move_line_id" invisible="1"/>
<field name="product_tracking" invisible="1"/>
@@ -300,23 +308,47 @@
<field name="alert_ids" invisible="1"/>
</group>
<group>
<field name="picking_id"
attrs="{'invisible': [('quality_state', 'in', ('pass', 'fail')), ('picking_id', '=', False)]}"/>
<field name="point_id"/>
<field name="measure_on" attrs="{'readonly': [('point_id', '!=', False)]}"/>
<field string="Type" name="test_type_id" options="{'no_open': True, 'no_create': True}"
attrs="{'readonly': [('point_id', '!=', False)]}"/>
<field name="picking_id"
attrs="{'invisible': [('quality_state', 'in', ('pass', 'fail')), ('picking_id', '=', False)]}"/>
<field name="control_date" invisible="1"/>
<field name="partner_id" string="Partner"
attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="team_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="user_id" string="Control Person" invisible="1"/>
<field name="partner_id" string="Partner"
attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="measure_operator" string="操机员"/>
</group>
</group>
<group attrs="{'invisible': [('test_type', '!=', 'picture')]}">
<field name="picture" widget="image"/>
</group>
<notebook>
<!-- 增加page页测量、出厂检验报告、2D加工图纸、质检标准、发布历史它们均在is_out_check为True时显示 -->
<!-- 测量page内有一个添加测量值按钮点击可以新增一行测量值新增的行在tree视图中显示显示的列有序号、检测项目、测量值1、测量值2、测量值3、测量值4、测量值5、判定、备注。其中检测项目、测量值1、测量值2、测量值3、测量值4、测量值5为必填项判定为下拉框默认选中合格备注为文本框。 -->
<page string="测量" name="measure" attrs="{'invisible': [('is_out_check', '=', False)]}">
<div class="o_row">
<button name="add_measure_line" type="object" class="btn-primary" string="添加测量值"/>
<button name="remove_measure_line" type="object" class="btn-primary" string="删除测量值"/>
<button name="%(quality_control.import_complex_model_wizard)d" string="上传"
type="action"
class="btn-primary"
attrs="{'force_show':1}"
context="{'default_model_name': 'quality.check.measure.line'}"/>
</div>
<br/>
<div class="o_row">
<field name="measure_line_ids" widget="tree"/>
</div>
</page>
<page string="出厂检验报告" name="out_check" attrs="{'invisible': [('is_out_check', '=', False)]}">
<field name="report_content" widget="adaptive_viewer"/>
</page>
<page string="Notes" name="notes">
<group>
<field name="report_result"/>
@@ -326,6 +358,18 @@
</group>
</page>
<page string="发布历史" name="release_history" attrs="{'invisible': [('is_out_check', '=', False)]}">
<field name="report_history_ids">
<tree>
<field name="sequence"/>
<field name="report_number_id"/>
<field name="action"/>
<field name="document_status"/>
<field name="operation_time"/>
<field name="operator"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">

View File

@@ -2,3 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import quality_check_wizard
from . import import_complex_model
from . import quality_wizard

View File

@@ -0,0 +1,421 @@
# -*- coding: utf-8 -*-
import base64
import logging
import os
import traceback
from datetime import datetime
from io import BytesIO
from openpyxl import load_workbook
import pandas as pd
import xlrd
from odoo import fields, models, api, Command, _
# from odoo.exceptions import ValidationError
from odoo.exceptions import UserError
from datetime import datetime, timedelta
from odoo.http import request
_logger = logging.getLogger(__name__)
class ImportComplexModelWizard(models.TransientModel):
_name = 'quality.check.import.complex.model.wizard'
file_data = fields.Binary("数据文件")
model_name = fields.Char(string='Model Name')
field_basis = fields.Char(string='Field Basis')
def get_model_column_name_labels(self, model):
fields_info = model.fields_get()
# 提取字段名称和展示名称
field_labels = {field_info.get('string'): field_info for field_name, field_info in fields_info.items()}
return field_labels
def field_name_mapping(selfcolumn, column, field_data):
return {}
@api.model
def page_to_field(self, field_data):
return {}
def count_continuous_none(self, df):
# 用于存储间断的连续空值的区间
none_intervals = []
# if pd.isna(val):
# 遍历数据并查找连续的 None
start = 0
num = 0
for index, row in df.iterrows():
if pd.isna(row[0]):
continue
else:
# 记录区间
if num == 0:
start = index
num = 1
else:
end = index
none_intervals.append({'start': start, 'end': index})
start = end
# start = None # 重置
# 检查最后一个区间
if len(df) - start >= 1:
none_intervals.append({'start': start, 'end': len(df)})
return none_intervals
def find_repeated_ranges(self, data):
# 判断内联列表范围有column列的为内联列表字段
if not data:
return []
repeats = []
start_index = 0
current_value = data[0]
for i in range(1, len(data)):
if data[i] == current_value:
continue
else:
if i - start_index > 1: # 有重复
repeats.append({'start': start_index, 'end': i, 'column': data[i - 1]})
# repeats.append((current_value, start_index, i - 1))
current_value = data[i]
start_index = i
# 检查最后一段
if len(data) - start_index > 1:
repeats.append({'start': start_index, 'end': i, 'column': data[i - 1]})
# repeats.append((current_value, start_index, len(data) - 1))
return repeats
def import_data(self):
"""导入Excel数据"""
if not self.file_data:
raise UserError(_('请先上传Excel文件'))
# 解码文件数据
file_content = base64.b64decode(self.file_data)
try:
# 使用xlrd读取Excel文件
workbook = xlrd.open_workbook(file_contents=file_content)
sheet = workbook.sheet_by_index(0)
# 检查表头是否符合预期(忽略星号)
expected_headers = ['产品名称', '图号', '检测项目', '测量值1', '测量值2', '测量值3', '测量值4', '测量值5', '判定', '备注']
actual_headers = sheet.row_values(0)
# 处理合并单元格的情况
# 获取所有合并单元格
merged_cells = []
for crange in sheet.merged_cells:
rlo, rhi, clo, chi = crange
if rlo == 0: # 只关注第一行(表头)的合并单元格
merged_cells.append((clo, chi))
# 清理表头(移除星号和处理空值)
actual_headers_clean = []
for i, header in enumerate(actual_headers):
# 检查当前列是否在合并单元格范围内但不是合并单元格的起始列
is_merged_not_first = any(clo < i < chi for clo, chi in merged_cells)
if is_merged_not_first:
# 如果是合并单元格的非起始列,跳过
continue
# 处理表头文本
if isinstance(header, str):
header = header.replace('*', '').strip()
if header: # 只添加非空表头
actual_headers_clean.append(header)
# 检查表头数量
if len(actual_headers_clean) < len(expected_headers):
raise UserError(_('表头列数不足,请使用正确的模板文件'))
# 检查表头顺序(忽略星号)
for i, header in enumerate(expected_headers):
if i >= len(actual_headers_clean) or header != actual_headers_clean[i]:
actual_header = actual_headers_clean[i] if i < len(actual_headers_clean) else "缺失"
raise UserError(_('表头顺序不正确,第%s列应为"%s",但实际为"%s"') %
(i + 1, header, actual_header))
# 获取当前活动的quality.check记录
active_id = self.env.context.get('active_id')
quality_check = self.env['quality.check'].browse(active_id)
if not quality_check:
raise UserError(_('未找到相关的质检记录'))
# 记录是否有有效数据被导入
valid_data_imported = False
# 从第二行开始读取数据(跳过表头)
for row_index in range(1, sheet.nrows):
row = sheet.row_values(row_index)
# 检查行是否有数据
if not any(row):
continue
if row[2] == '':
continue
# 创建quality.check.measure.line记录
measure_line_vals = {
'check_id': quality_check.id,
'sequence': len(quality_check.measure_line_ids) + 1,
'product_name': str(row[0]) if row[0] else '', # 产品名称列
'drawing_no': str(row[1]) if row[1] else '', # 图号列
'measure_item': row[2] or '', # 检测项目列
'measure_value1': str(row[4]) if row[4] else '', # 测量值1
'measure_value2': str(row[5]) if row[5] else '', # 测量值2
'measure_value3': str(row[6]) if len(row) > 6 and row[6] else '', # 测量值3
'measure_value4': str(row[7]) if len(row) > 7 and row[7] else '', # 测量值4
'measure_value5': str(row[8]) if len(row) > 8 and row[8] else '', # 测量值5
'measure_result': 'NG' if row[9] == 'NG' else 'OK', # 判定列
'remark': row[10] if len(row) > 10 and row[10] else '', # 备注列
}
self.env['quality.check.measure.line'].create(measure_line_vals)
valid_data_imported = True
# 检查是否有有效数据被导入
if not valid_data_imported:
raise UserError(_('表格中没有有效数据行可导入,请检查表格内容'))
# 返回关闭弹窗的动作
return {
'type': 'ir.actions.act_window_close'
}
except Exception as e:
_logger.error("导入Excel数据时出错: %s", str(e))
_logger.error(traceback.format_exc())
raise UserError(_('导入失败: %s') % str(e))
def process_first_line(self, df_columns, column_labels, field_data):
columns = []
last_column_name = None
for index, column in enumerate(df_columns):
if not column_labels.get(column):
if 'Unnamed' in column:
columns.append(last_column_name)
else:
field_name_map = self.page_to_field(field_data)
field_name = field_name_map.get(column)
if field_name:
columns.append(field_name)
last_column_name = field_name
else:
columns.append(column)
last_column_name = column
else:
columns.append(column)
last_column_name = column
return columns
def process_inline_list_column(self, columns, first_row, repeat_list):
for index, repeat_map in enumerate(repeat_list):
start = repeat_map.get('start')
end = int(repeat_map.get('end'))
if len(repeat_list) - 1 == index:
end = end + 1
for i in range(start, end):
field_name = columns[i]
embedded_fields = first_row[i]
if pd.isna(embedded_fields):
columns[i] = field_name
else:
columns[i] = field_name + '?' + embedded_fields
def process_data_list(self, model, data_list, column_labels, sheet):
try:
for index, data in enumerate(data_list):
# 转换每行数据到模型data = {dict: 11} {'刀具物料': '刀片', '刀尖特征': '刀尖倒角', '刀片形状': '五边形', '刀片物料参数': [{'刀片物料参数?内接圆直径IC/D(mm)': 23, '刀片物料参数?刀尖圆弧半径RE(mm)': 2, '刀片物料参数?刀片牙型': '石油管螺纹刀片', '刀片物料参数?切削刃长(mm)': 3, '刀片物料参数?厚度(mm)': 12, '刀片物料参数?后角(mm)': 4, '刀片物料参数?安装孔直径D1(mm)': 2, '刀片物料参数?有无断屑槽': '无', '刀片物料参数?物…视图
self.import_model_record(model, data, column_labels, sheet)
except Exception as e:
traceback_error = traceback.format_exc()
logging.error('批量导入失败sheet %s' % sheet)
logging.error('批量导入失败 : %s' % traceback_error)
raise UserError(e)
@api.model
def process_model_record_data(self, model, data, column_labels, sheet):
pass
def import_model_record(self, model, data, column_labels, sheet):
self.process_model_record_data(model, data, column_labels, sheet)
model_data = self.convert_column_name(data, column_labels)
logging.info('批量导入模型{} 数据: {}'.format(model, model_data))
new_model = model.create(model_data)
self.model_subsequent_processing(new_model, data)
@api.model
def model_subsequent_processing(self, model, data):
pass
def convert_column_name(self, data, column_labels):
tmp_map = {}
for key, value in data.items():
if not column_labels.get(key):
continue
if key == "执行标准":
print('fqwioiqwfo ', value, column_labels)
field_info = column_labels.get(key)
tmp_map[field_info.get('name')] = self.process_field_data(value, field_info)
return tmp_map
def process_field_data(self, value, field_info):
relation_field_types = ['many2one', 'one2many', 'many2many']
field_type = field_info.get('type')
if field_type not in relation_field_types:
return value
if field_type == 'Boolean':
if value == '' or value == '':
return True
else:
return False
if field_type == 'many2one':
relation_info = self.env[field_info.get('relation')].sudo().search([('name', '=', value)], limit=1)
if relation_info:
return int(relation_info)
if isinstance(value, list):
return self.process_basic_data_list(value, field_info)
else:
relation_info = self.env[field_info.get('relation')].sudo().search([('name', '=', value)], limit=1)
if relation_info:
return [Command.link(int(relation_info))]
def process_basic_data_list(self, value, field_info):
if self.is_basic_data_list(value):
return [
Command.link(
int(self.env[field_info.get('relation')].sudo().search([('name', '=', element)], limit=1)))
for element in value
]
else:
association_column_labels = self.get_model_column_name_labels(
self.env[field_info.get('relation')].sudo())
data_list = [
{column_name.split('?')[1]: column_value
for column_name, column_value in association_column_data.items()
if
len(column_name.split('?')) == 2 and association_column_labels.get(column_name.split('?')[1])}
for association_column_data in value
]
data_list = [self.convert_column_name(element, association_column_labels) for element in data_list]
return [
Command.create(
column_map
) for column_map in data_list
]
def get_remaining_ranges(self, ranges, full_list_length):
# 创建一个集合用于存储被覆盖的索引
covered_indices = set()
# 遍历范围集合,标记覆盖的索引
for r in ranges:
start = r['start']
end = r['end']
# 将该范围内的索引加入集合
covered_indices.update(range(start, end + 1))
# 计算未覆盖的范围
remaining_ranges = []
start = None
for i in range(full_list_length):
if i not in covered_indices:
if start is None:
start = i # 开始新的未覆盖范围
else:
if start is not None:
# 记录当前未覆盖范围
remaining_ranges.append({'start': start, 'end': i - 1})
start = None # 重置
# 处理最后一个范围
if start is not None:
remaining_ranges.append({'start': start, 'end': full_list_length - 1})
return remaining_ranges
def is_basic_data_list(self, lst):
basic_types = (int, float, str, bool)
lst_len = len(lst)
if lst_len < 1:
return False
if isinstance(lst[0], basic_types):
return True
return False
def index_retrieve_data(self, df, range_map, i, data_map):
for remaining_map in range_map:
relation_column_data_map = {}
for j in range(remaining_map.get('start'), int(remaining_map.get('end')) + 1):
value = df.iat[i, j]
if pd.isna(value):
continue
if remaining_map.get('column'):
relation_column_data_map.update({df.columns[j]: value})
else:
if not data_map.get(df.columns[j]):
data_map.update({df.columns[j]: value})
elif isinstance(data_map.get(df.columns[j]), list):
data_map.get(df.columns[j]).append(value)
else:
lst = [data_map.get(df.columns[j]), value]
data_map.update({df.columns[j]: lst})
if relation_column_data_map and len(relation_column_data_map) > 0:
data_map.setdefault(remaining_map.get('column'), []).append(relation_column_data_map)
def parse_excel_data_matrix(self, df, repeat_list):
row_interval_list = self.count_continuous_none(df)
remaining_ranges = self.get_remaining_ranges(repeat_list, len(df.columns))
data_list = []
for row_interval_map in row_interval_list:
data_map = {}
for index in range(row_interval_map.get('start'), int(row_interval_map.get('end'))):
if index == 0:
self.index_retrieve_data(df, remaining_ranges, index, data_map)
else:
self.index_retrieve_data(df, remaining_ranges, index, data_map)
self.index_retrieve_data(df, repeat_list, index, data_map)
if len(data_map) > 0:
data_list.append(data_map)
return data_list
def saadqw(self):
excel_template = self.env['excel.template'].sudo().search([('model_id.model', '=', self.model_name)], limit=1)
file_content = base64.b64decode(excel_template.file_data)
return {
'type': 'ir.actions.act_url',
'url': 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,{file_content}',
'target': 'self',
'download': excel_template.file_name,
}
# return request.make_response(
# file_content,
# headers=[
# ('Content-Disposition', f'attachment; filename="{excel_template.file_name}"'),
# ('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
# ]
# )
def download_excel_template(self):
excel_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + '/quality_control/static/src/binary/出厂检验报告上传模版.xlsx'
value = dict(
type='ir.actions.act_url',
target='self',
url=excel_url,
)
return value

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="quality_check_import_complex_model_wizard_form" model="ir.ui.view">
<field name="name">请导入数据文件</field>
<field name="model">quality.check.import.complex.model.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="file_data" widget="binary" options="{'accepted_file_extensions': '.xlsx'}"/>
</group>
<footer>
<button string="确认导入" name="import_data" type="object" class="btn-primary"/>
<button string="取消" class="btn-primary" special="cancel"/>
<button string="模板文件下载" name="download_excel_template" type="object" class="btn-primary"/>
</footer>
</form>
</field>
</record>
<record id="import_complex_model_wizard" model="ir.actions.act_window">
<field name="name">导入模型数据</field>
<field name="type">ir.actions.act_window</field>
<!-- <field name="res_model">up.select.wizard</field>-->
<field name="res_model">quality.check.import.complex.model.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="quality_check_import_complex_model_wizard_form"/>
<field name="target">new</field>
<!-- <field name="context"></field>-->
</record>
</odoo>

View File

@@ -113,6 +113,10 @@ class QualityCheckWizard(models.TransientModel):
default_current_check_id=self.current_check_id.id,
)
return action
# 对于成品出库的出厂检验报告,增加发布按钮
def publish(self):
self.current_check_id._do_publish_implementation()
class PickingCheckCancelWizard(models.TransientModel):

View File

@@ -79,7 +79,10 @@
attrs="{'invisible': ['|', ('quality_state', '!=', 'none'), ('test_type', '!=', 'passfail')]}" data-hotkey="x"/>
<button name="action_generate_previous_window" type="object" class="btn-secondary" string="Previous" attrs="{'invisible': [('position_current_check', '=', 1)]}"/>
<button name="action_generate_next_window" type="object" class="btn-secondary" string="Next" attrs="{'invisible': [('is_last_check', '=', True)]}"/>
<button string="发布" name="publish" type="object" class="btn-primary"
attrs="{'invisible': ['|', ('quality_state', '!=', 'none'), ('test_type', '!=', 'factory_inspection')]}" data-hotkey="p"/>
<button string="Cancel" class="btn btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
class QualityCheckPublishWizard(models.TransientModel):
_name = 'quality.check.publish.wizard'
_description = '质检报告发布确认'
check_id = fields.Many2one('quality.check', string='质检单', required=True)
product_name = fields.Char('产品名称', readonly=True)
total_qty = fields.Char('总数量', readonly=True)
check_qty = fields.Char('检验数', readonly=True)
measure_count = fields.Integer('测量件数', readonly=True)
item_count = fields.Integer('检验项目数', readonly=True)
old_report_name = fields.Char('旧出厂检验报告编号', readonly=True)
publish_status = fields.Selection([('draft', '草稿'), ('published', '已发布'), ('canceled', '已撤销')], string='发布状态', readonly=True)
def action_confirm_publish(self):
"""确认发布"""
self.ensure_one()
return self.check_id._do_publish_implementation()

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_quality_check_publish_wizard_form" model="ir.ui.view">
<field name="name">quality.check.publish.wizard.form</field>
<field name="model">quality.check.publish.wizard</field>
<field name="arch" type="xml">
<form string="发布确认">
<div class="alert alert-info" role="alert" style="margin-bottom:0px;">
<p>您即将发布出厂检验报告:产品<strong><field name="product_name" class="oe_inline"/></strong>,总数量<strong><field name="total_qty" class="oe_inline"/></strong>,检验数<strong><field name="check_qty" class="oe_inline"/></strong>,测量<strong><field name="measure_count" class="oe_inline"/></strong>件,检验项目<strong><field name="item_count" class="oe_inline"/></strong>项。</p>
<field name="publish_status" invisible="1"/>
<!-- 状态为draft时显示 -->
<div attrs="{'invisible': [('publish_status', '!=', 'draft')]}">
<span style="font-weight:bold;">
注意:发布后所有用户可扫描下载本报告
</span>
</div>
<!-- 状态不为draft时显示 -->
<div attrs="{'invisible': [('publish_status', '=', 'draft')]}">
<span style="font-weight:bold;">
注意:已发布的报告
<field name="old_report_name" readonly="1"
style="color:red;"
attrs="{'invisible': [('old_report_name', '=', False)]}"/>
<span style="color:red;"
attrs="{'invisible': [('old_report_name', '!=', False)]}">(未知报告编号)</span>
可能已被客户下载
</span>
</div>
</div>
<footer>
<button name="action_confirm_publish" string="发布" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@@ -94,6 +94,12 @@ class SFSaleOrderCancelWizard(models.TransientModel):
if mo_quality_checks:
mo_quality_checks.write({'quality_state': 'cancel'})
# 取消制造订单的排程单
mo_plan_orders = self.env['sf.production.plan'].search([
('origin', '=', order.name)])
if mo_plan_orders:
mo_plan_orders.write({'state': 'cancel'})
# 取消制造订单的子制造订单
child_mo_ids = self.env['mrp.production'].search([
('origin', '=', mo.name)
@@ -182,6 +188,12 @@ class SFSaleOrderCancelLine(models.TransientModel):
'progress': '加工中',
'assigned': '就绪'
}
plan_map_dict = {
'draft': '待排程',
'done': '已排程',
'processing': '加工中',
'finished': '已完成',
'cancel': '已取消'}
module_name_dict = {
'purchase': '采购',
@@ -288,6 +300,31 @@ class SFSaleOrderCancelLine(models.TransientModel):
}
lines.append(self.create(vals))
# 检查所有的排程单
sf_plan_orders = self.env['sf.production.plan'].search([
('origin', '=', order.name)])
if sf_plan_orders:
p1 = 0
for plan_order in sf_plan_orders:
if not plan_order.product_id.default_code:
continue
p1 += 1
vals = {
'wizard_id': wizard_id,
'sequence': sequence,
'category': '排程',
'doc_name': '排程单',
'operation_type': '',
'doc_number': plan_order.name,
'line_number': p1,
'product_name': f'[{plan_order.product_id.default_code}] {plan_order.product_id.name}',
'quantity': 1,
'doc_state': plan_map_dict.get(plan_order.state, plan_order.state),
'cancel_reason': '已有异动' if plan_order.state not in ['draft', 'cancel'] else ''
}
lines.append(self.create(vals))
sequence += 1
# 检查组件的制造单
# component_mos = self.env['mrp.production'].search([
# ('origin', '=', mo.name)])

View File

@@ -16,10 +16,15 @@
'depends': ['quality_control', 'web_widget_model_viewer', 'sf_manufacturing','jikimo_attachment_viewer'],
'data': [
'security/ir.model.access.csv',
'data/check_standards.xml',
'data/documents_data.xml',
'data/insepection_report_template.xml',
'data/report_actions.xml',
'views/view.xml',
'views/quality_cnc_test_view.xml',
'views/mrp_workorder.xml',
'views/quality_check_view.xml',
'views/quality_company.xml',
'wizard/check_picking_wizard_view.xml',
],

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="test_type_factory_inspection" model="quality.point.test_type">
<field name="name">出厂检验报告</field>
<field name="technical_name">factory_inspection</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- 创建出厂检验报告文件夹 -->
<record id="documents_purchase_contracts_folder" model="documents.folder">
<field name="name">出厂检验报告</field>
<field name="description">存放出厂检验报告相关文件</field>
<field name="sequence">11</field>
</record>
<!-- 创建出厂检验报告文件夹下的子文件夹-已发布 -->
<record id="documents_purchase_contracts_folder_published" model="documents.folder">
<field name="name">已发布</field>
<field name="parent_folder_id" ref="documents_purchase_contracts_folder"/>
<field name="sequence">1</field>
</record>
<!-- 创建出厂检验报告文件夹下的子文件夹-废弃 -->
<record id="documents_purchase_contracts_folder_canceled" model="documents.folder">
<field name="name">废弃</field>
<field name="parent_folder_id" ref="documents_purchase_contracts_folder"/>
<field name="sequence">2</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 定义页眉模板 -->
<template id="report_quality_header">
<div class="header">
<div class="pdf-viewer-toolbar" style="display:flex;justify-content:space-between;align-items:center;">
<img t-if="o.company_id.favicon" t-att-src="image_data_uri(o.company_id.favicon)" style="max-height: 70px;" alt="Logo"/>
<div class="text-center">
<h2>出厂检验报告</h2>
</div>
<div class="float-right" style="text-align: right;">
<!-- 使用公开访问URL的二维码 -->
<img t-att-src="'/report/barcode/QR/%s' % o.get_report_url()" style="width:80px;height:80px"/>
<div style="font-size: 20px;">
<strong>报告编号: </strong><span t-field="o.name"/>
</div>
<!-- 添加扫码提示 -->
<div style="font-size: 12px; margin-top: 5px;">
<strong>扫描二维码查看PDF报告</strong>
</div>
</div>
</div>
</div>
</template>
<!-- 定义页脚模板 -->
<template id="report_quality_footer">
<div class="footer">
<div class="row">
<div class="col-6">
<p>售后服务: <span t-field="o.company_id.phone"/></p>
<p>公司名称: <span t-field="o.company_id.name"/></p>
<p>公司网址: <span t-field="o.company_id.website"/></p>
<p>公司邮箱: <span t-field="o.company_id.email"/></p>
</div>
<div class="col-6">
<p>加工工厂: <span t-field="o.company_id.factory_name"/></p>
</div>
</div>
<div class="text-center">
<span><span class="page"/> 页/共 <span class="topage"/></span>
</div>
</div>
</template>
<template id="report_quality_inspection">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.basic_layout">
<!-- 调用自定义页眉 -->
<t t-call="sf_quality.report_quality_header"/>
<div class="page">
<!-- <div class="col-6">
<div t-if="o.company_id.favicon" class="float-right">
<img t-att-src="image_data_uri(o.company_id.favicon)" style="max-height: 45px;" alt="Logo"/>
</div>
</div> -->
<!-- </div> -->
<table class="table table-sm o_main_table mt-4" style="border: 1px solid black;">
<tr>
<td style="width: 15%; border: 1px solid black;"><strong>产品名称:</strong></td>
<td style="width: 35%; border: 1px solid black;"><span t-field="o.product_id.name"/></td>
<td style="width: 15%; border: 1px solid black;"><strong>材料:</strong></td>
<td style="width: 35%; border: 1px solid black;"><span t-field="o.material_name"/></td>
</tr>
<tr>
<td style="border: 1px solid black;"><strong>图号:</strong></td>
<td style="border: 1px solid black;"><span t-field="o.part_number"/></td>
<td style="border: 1px solid black;"><strong>日期:</strong></td>
<td style="border: 1px solid black;"><span t-field="o.write_date"/></td>
</tr>
<tr>
<td style="border: 1px solid black;"><strong>总数量:</strong></td>
<td style="border: 1px solid black;"><span t-field="o.total_qty"/></td>
<td style="border: 1px solid black;"><strong>检验数量:</strong></td>
<td style="border: 1px solid black;"><span t-field="o.check_qty"/></td>
</tr>
</table>
<h4 class="text-center mt-4">检验结果</h4>
<div class="" style="position: relative;">
<table class="table table-sm mt-2" style="border: 1px solid black;">
<thead>
<tr>
<th style="border: 1px solid black;" class="text-center" rowspan="2">检测项目<br/>(图示尺寸)</th>
<th style="border: 1px solid black;" t-att-colspan="o.column_nums" class="text-center">测量值</th>
<th style="border: 1px solid black; vertical-align: middle;" class="text-center" rowspan="2">判定</th>
<th style="border: 1px solid black; vertical-align: middle;" class="text-center" rowspan="2">备注</th>
</tr>
<tr>
<!-- <th style="border: 1px solid black;"></th> -->
<th style="border: 1px solid black;" t-if="o.column_nums >= 1" class="text-center">1</th>
<th style="border: 1px solid black;" t-if="o.column_nums >= 2" class="text-center">2</th>
<th style="border: 1px solid black;" t-if="o.column_nums >= 3" class="text-center">3</th>
<th style="border: 1px solid black;" t-if="o.column_nums >= 4" class="text-center">4</th>
<th style="border: 1px solid black;" t-if="o.column_nums >= 5" class="text-center">5</th>
<!-- <th style="border: 1px solid black;"></th>
<th style="border: 1px solid black;"></th> -->
</tr>
</thead>
<tbody>
<tr t-foreach="o.measure_line_ids" t-as="line">
<td style="border: 1px solid black;" class="text-center"><span t-field="line.measure_item"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 1" class="text-center"><span t-field="line.measure_value1"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 2" class="text-center"><span t-field="line.measure_value2"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 3" class="text-center"><span t-field="line.measure_value3"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 4" class="text-center"><span t-field="line.measure_value4"/></td>
<td style="border: 1px solid black;" t-if="o.column_nums >= 5" class="text-center"><span t-field="line.measure_value5"/></td>
<td style="border: 1px solid black;" class="text-center"><span t-field="line.measure_result"/></td>
<td style="border: 1px solid black;" class="text-center"><span t-field="line.remark"/></td>
</tr>
</tbody>
</table>
<img src="/sf_quality/static/img/pass.png" style="width: 200px; height: 200px;position: absolute; bottom: 20px; right: 20%;"/>
</div>
<div class="row mt-4">
<div class="col-12">
<h5>检验结论:
<span t-if="o.report_result == 'OK'" style="margin-left: 20px;">☑ 合格</span>
<span t-else="" style="margin-left: 20px;">□ 合格</span>
<span t-if="o.report_result == 'NG'" style="margin-left: 40px;">☑ 不合格</span>
<span t-else="" style="margin-left: 40px;">□ 不合格</span>
</h5>
</div>
</div>
<div class="row mt-4">
<div class="col-6">
<p><strong>操作员: </strong> <span t-field="o.measure_operator"/></p>
</div>
<div class="col-6">
<p><strong>质检员: </strong> <span t-field="o.quality_manager"/></p>
</div>
</div>
<!-- 添加合格标签 -->
<div class="row mt-5">
<div class="col-12 text-center">
<p>(以下空白)</p>
</div>
</div>
<!-- 调用自定义页脚 -->
<t t-call="sf_quality.report_quality_footer"/>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_report_quality_inspection" model="ir.actions.report">
<field name="name">出厂检验报告</field>
<field name="model">quality.check</field> <!-- 请替换为实际的模型名称 -->
<field name="report_type">qweb-pdf</field>
<field name="report_name">sf_quality.report_quality_inspection</field>
<field name="report_file">sf_quality.report_quality_inspection</field>
<field name="print_report_name">'QC-' + object.name + '.pdf'</field>
<field name="binding_model_id" ref="model_quality_check"/> <!-- 请替换为实际的模型ID引用 -->
<field name="binding_type">report</field>
<field name="attachment">'QC-'+object.name+'-'+str(object.write_date)+'.pdf'</field>
<field name="attachment_use">True</field>
</record>
<!-- 定义HTML预览报告动作 -->
<record id="action_report_quality_inspection_preview" model="ir.actions.report">
<field name="name">预览检验报告</field>
<field name="model">quality.check</field>
<field name="report_type">qweb-html</field>
<field name="report_name">sf_quality.report_quality_inspection</field>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@@ -6,3 +6,4 @@ from . import quality
from . import quality_cnc_test
from . import mrp_workorder
from . import stock
from . import quality_company

View File

@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class SfQualityPoint(models.Model):
_inherit = 'quality.point'
_rec_name = 'title'
product_ids = fields.Many2many(
'product.product', string='适用产品',
@@ -15,4 +16,14 @@ class SfQualityPoint(models.Model):
operation_id = fields.Many2one(
'mrp.routing.workcenter', 'Step', check_company=True,
domain="[('is_outsource', '=', False),('company_id', 'in', (company_id, False))]")
@api.onchange('test_type_id')
def _onchange_test_type_id(self):
"""
如果类型选择了出厂检验报告检查measure_on的值是否为product如果为product则类型的值不变如果
不是,则提示错误
"""
if self.test_type_id.name == '出厂检验报告':
if self.measure_on != 'product':
raise ValidationError('出厂检验报告的测量对象必须为产品')

View File

@@ -0,0 +1,8 @@
from odoo import models, fields
# 为公司增加字段
class Company(models.Model):
_inherit = 'res.company'
factory_name = fields.Char('加工工厂')

View File

@@ -5,6 +5,17 @@ class StockPicking(models.Model):
_inherit = 'stock.picking'
def button_validate(self):
res = super(StockPicking, self).button_validate()
"""
出厂检验报告上传
"""
out_quality_check = self.env['quality.check'].search(
[('picking_id', '=', self.id), ('test_type_id.name', '=', '出厂检验报告')])
if not out_quality_check.is_factory_report_uploaded:
if out_quality_check and self.state == 'assigned':
out_quality_check.upload_factory_report()
"""
调拨单若关联了质量检查单,验证调拨单时,应校验是否有不合格品,若存在,应弹窗提示:
“警告存在不合格产品XXXX n 件、YYYYY m件继续调拨请点“确认”否则请取消
@@ -36,4 +47,4 @@ class StockPicking(models.Model):
'default_fail_check_text': f'警告:存在不合格产品{fail_check_text},继续调拨请点“确认”,否则请取消?',
'again_validate': True}
}
return super(StockPicking, self).button_validate()
return res

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -39,11 +39,11 @@
</group>
</group>
</page>
<page string="2D图纸" attrs="{'invisible': [('production_id', '=', False)]}">
<field name="machining_drawings" string="" widget="adaptive_viewer"/>
<page string="2D加工图纸" attrs="{'invisible': [('categ_type', 'not in', ['成品', '坯料'])]}">
<field name='machining_drawings' widget="adaptive_viewer"/>
</page>
<page string="客户质量标准" attrs="{'invisible': [('production_id', '=', False)]}">
<field name="quality_standard" string="" widget="adaptive_viewer"/>
<page string="质检标准" attrs="{'invisible': [('categ_type', 'not in', ['成品', '坯料'])]}">
<field name='quality_standard' widget="adaptive_viewer"/>
</page>
<page string="其他"
attrs="{'invisible': ['|',('quality_state', 'not in', ['pass', 'fail']), ('production_id', '=', False)]}">
@@ -54,6 +54,7 @@
</page>
</xpath>
<xpath expr="//header//button[@name='do_pass'][1]" position="attributes">
<attribute name="attrs">{'invisible': [('is_out_check', '=', True)]}</attribute>
<attribute name="string">合格</attribute>
</xpath>
<xpath expr="//header//button[@name='do_pass'][2]" position="attributes">
@@ -61,12 +62,32 @@
<attribute name="string">合格</attribute>
</xpath>
<xpath expr="//header//button[@name='do_fail'][1]" position="attributes">
<attribute name="attrs">{'invisible': [('is_out_check', '=', True)]}</attribute>
<attribute name="string">不合格</attribute>
</xpath>
<xpath expr="//header//button[@name='do_fail'][2]" position="attributes">
<attribute name="attrs">{'invisible': ['|',('quality_state', '!=', 'pass'),('work_state','in', ('done', 'rework'))]}</attribute>
<attribute name="string">不合格</attribute>
</xpath>
<xpath expr="//header" position="inside">
<field name="is_out_check" invisible="1"/>
<field name="publish_status" invisible="1"/>
<button name="%(sf_quality.action_report_quality_inspection_preview)d"
string="预览"
type="action"
class="oe_highlight" attrs="{'invisible': [('is_out_check', '=', False)]}"/>
<!-- --><!-- 如果还需要打印按钮 -->
<!-- <button name="%(sf_quality.action_report_quality_inspection)d" -->
<!-- string="打印报告" -->
<!-- type="action"/> -->
<!-- <button name="do_preview" string="预览" type="object" class="btn-primary" attrs="{'invisible': [('is_out_check', '=', False)]}"/> -->
<button name="do_publish" string="发布" type="object" class="btn-primary" attrs="{'invisible': ['|', ('is_out_check', '=', False), ('publish_status', '!=', 'draft')]}"/>
<!-- <button name="get_report_url" string="ceshi" type="object" class="btn-primary"/> -->
<!-- <button name="upload_factory_report" string="upload_factory_report" type="object" class="btn-primary"/> -->
<button name="do_cancel_publish" string="取消发布" type="object" class="btn-primary" attrs="{'invisible': ['|',('is_out_check', '=', False), ('publish_status', '!=', 'published')]}"/>
<button name="do_re_publish" string="重新发布" type="object" class="btn-primary" attrs="{'invisible': ['|', ('is_out_check', '=', False), ('publish_status', '!=', 'canceled')]}"/>
</xpath>
</field>
</record>

View File

@@ -0,0 +1,13 @@
<odoo>
<record id="sf_quality_company_view" model="ir.ui.view">
<field name="name">sf.quality.company.view</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="factory_name"/>
</xpath>
</field>
</record>
</odoo>